Atualização 3 | Tratativa de erros no GR, Ajustes sobre o ambiente do Financeiro + Workspace
This commit is contained in:
parent
5327a10251
commit
6d7ec7c9aa
|
|
@ -25,5 +25,10 @@
|
|||
"feature": "Corre\u00e7\u00e3o nas rotas de edi\u00e7\u00e3o de Bases e Respons\u00e1veis do GR",
|
||||
"status": "active",
|
||||
"timestamp": "2026-02-08"
|
||||
},
|
||||
{
|
||||
"feature": "Corrigido erro nos cards de Pend\u00eancias/Revis\u00e3o do GR para Despachantes",
|
||||
"status": "active",
|
||||
"timestamp": "2026-02-08"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import re
|
||||
import sys
|
||||
|
||||
def refactor_file(filepath):
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Pattern to find:
|
||||
# try {
|
||||
# const response = await ...
|
||||
# return ...
|
||||
# } catch (error) {
|
||||
# throw new Error(...) or throw error
|
||||
# }
|
||||
|
||||
# This regex matches the try/catch block and replaces it with the inner code
|
||||
# It accounts for indentation and multi-line content
|
||||
pattern = r'try\s*\{([\s\S]*?)\}\s*catch\s*\(error\)\s*\{[\s\S]*?\}'
|
||||
|
||||
def replacement(match):
|
||||
inner_code = match.group(1)
|
||||
# Strip one level of indentation (usually 2 spaces)
|
||||
lines = inner_code.split('\n')
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
if line.startswith(' '):
|
||||
new_lines.append(line[2:])
|
||||
elif line.startswith(' '):
|
||||
new_lines.append(line[2:])
|
||||
else:
|
||||
new_lines.append(line)
|
||||
return '\n'.join(new_lines).strip()
|
||||
|
||||
new_content = re.sub(pattern, replacement, content)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(new_content)
|
||||
|
||||
if __name__ == "__main__":
|
||||
refactor_file(sys.argv[1])
|
||||
|
|
@ -24,6 +24,7 @@ export const AutoFillInput = ({
|
|||
displayField = "name",
|
||||
onSelect,
|
||||
onAddNew,
|
||||
addNewLabel = "Criar novo",
|
||||
className,
|
||||
icon: Icon = Search
|
||||
}) => {
|
||||
|
|
@ -160,7 +161,7 @@ export const AutoFillInput = ({
|
|||
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg text-xs font-black uppercase tracking-tighter text-rose-500 hover:bg-rose-500/10 transition-colors"
|
||||
>
|
||||
<Plus size={14} strokeWidth={3} />
|
||||
Criar novo serviço: "{query}"
|
||||
{addNewLabel}: "{query}"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -87,9 +87,9 @@ export const BoletoCreationDialog = ({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[95vh] h-fit md:h-auto overflow-hidden bg-[#0f172a] border-slate-800 text-white p-0 shadow-2xl z-[9999]">
|
||||
<DialogContent className="max-w-4xl max-h-[95vh] h-fit md:h-auto overflow-hidden bg-white dark:bg-[#0f172a] border-slate-200 dark:border-slate-800 text-slate-900 dark:text-white p-0 shadow-2xl z-[9999]">
|
||||
<DialogHeader className="p-6 pb-2">
|
||||
<DialogTitle className="text-white font-bold text-xl flex items-center gap-2">
|
||||
<DialogTitle className="text-slate-900 dark:text-white font-bold text-xl flex items-center gap-2">
|
||||
<Receipt className="w-6 h-6 text-blue-500" />
|
||||
Gerar {creationMode === 'AVULSO' ? 'Boleto Avulso' : creationMode === 'MENSAL' ? 'Boleto Mensal' : 'Agendamento'}
|
||||
</DialogTitle>
|
||||
|
|
@ -113,7 +113,7 @@ export const BoletoCreationDialog = ({
|
|||
type="date"
|
||||
value={formData.data_vencimento}
|
||||
onChange={(e) => setFormData({...formData, data_vencimento: e.target.value})}
|
||||
className="bg-slate-800/30 border-slate-700 text-white h-12 text-lg text-center"
|
||||
className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-12 text-lg text-center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -132,18 +132,18 @@ export const BoletoCreationDialog = ({
|
|||
setShowsEmpresaSuggestions(true);
|
||||
}}
|
||||
onFocus={() => setShowsEmpresaSuggestions(true)}
|
||||
className="bg-slate-800/50 border-slate-700 text-white pl-10 focus:border-blue-500 transition-all h-10 text-sm"
|
||||
className="bg-white dark:bg-slate-800/50 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white pl-10 focus:border-blue-500 transition-all h-10 text-sm"
|
||||
/>
|
||||
{showsEmpresaSuggestions && filteredEmpresasSuggestions.length > 0 && (
|
||||
<Card className="absolute z-[10000] w-full mt-1 bg-slate-800 border-slate-700 shadow-2xl overflow-hidden animate-in fade-in slide-in-from-top-1">
|
||||
<Card className="absolute z-[10000] w-full mt-1 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 shadow-2xl overflow-hidden animate-in fade-in slide-in-from-top-1">
|
||||
<ScrollArea className="h-48">
|
||||
{filteredEmpresasSuggestions.map((empresa, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="p-3 hover:bg-slate-700 cursor-pointer border-b border-slate-700/50 last:border-0 transition-colors"
|
||||
className="p-3 hover:bg-slate-50 dark:hover:bg-slate-700 cursor-pointer border-b border-slate-100 dark:border-slate-700/50 last:border-0 transition-colors"
|
||||
onClick={() => handleSelectEmpresa(empresa)}
|
||||
>
|
||||
<p className="text-sm font-bold text-white">{empresa.nome_exibicao || empresa.nome}</p>
|
||||
<p className="text-sm font-bold text-slate-900 dark:text-white">{empresa.nome_exibicao || empresa.nome}</p>
|
||||
<p className="text-[10px] text-slate-400 uppercase font-medium">
|
||||
{empresa.cpf_cnpj} • {empresa.email}
|
||||
</p>
|
||||
|
|
@ -161,7 +161,7 @@ export const BoletoCreationDialog = ({
|
|||
<Input
|
||||
value={formData.nome}
|
||||
onChange={(e) => setFormData({...formData, nome: e.target.value})}
|
||||
className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm"
|
||||
className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-1 md:col-span-3">
|
||||
|
|
@ -170,7 +170,7 @@ export const BoletoCreationDialog = ({
|
|||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
||||
className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm"
|
||||
className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -180,7 +180,7 @@ export const BoletoCreationDialog = ({
|
|||
<Input
|
||||
value={formData.cpf_cnpj}
|
||||
onChange={(e) => setFormData({...formData, cpf_cnpj: e.target.value})}
|
||||
className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm"
|
||||
className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-1 md:col-span-3">
|
||||
|
|
@ -188,7 +188,7 @@ export const BoletoCreationDialog = ({
|
|||
<Input
|
||||
value={formData.telefone}
|
||||
onChange={(e) => setFormData({...formData, telefone: e.target.value})}
|
||||
className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm"
|
||||
className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -198,7 +198,7 @@ export const BoletoCreationDialog = ({
|
|||
<Input
|
||||
value={formData.endereco}
|
||||
onChange={(e) => setFormData({...formData, endereco: e.target.value})}
|
||||
className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm"
|
||||
className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-1 md:col-span-2">
|
||||
|
|
@ -206,7 +206,7 @@ export const BoletoCreationDialog = ({
|
|||
<Input
|
||||
value={formData.cep}
|
||||
onChange={(e) => setFormData({...formData, cep: e.target.value})}
|
||||
className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm"
|
||||
className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -215,7 +215,7 @@ export const BoletoCreationDialog = ({
|
|||
<Input
|
||||
value={formData.bairro}
|
||||
onChange={(e) => setFormData({...formData, bairro: e.target.value})}
|
||||
className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm"
|
||||
className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-1 md:col-span-3">
|
||||
|
|
@ -223,7 +223,7 @@ export const BoletoCreationDialog = ({
|
|||
<Input
|
||||
value={formData.cidade}
|
||||
onChange={(e) => setFormData({...formData, city: e.target.value})}
|
||||
className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm"
|
||||
className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-1 md:col-span-1">
|
||||
|
|
@ -243,10 +243,10 @@ export const BoletoCreationDialog = ({
|
|||
value={formData.tipo_pessoa}
|
||||
onValueChange={(val) => setFormData({...formData, tipo_pessoa: val})}
|
||||
>
|
||||
<SelectTrigger className="bg-slate-800/30 border-slate-700 text-white h-9 text-sm">
|
||||
<SelectTrigger className="bg-slate-100 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[#1e293b] border-slate-700 text-white">
|
||||
<SelectContent className="bg-white dark:bg-[#1e293b] border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white">
|
||||
<SelectItem value="JURIDICA">Pessoa Jurídica</SelectItem>
|
||||
<SelectItem value="FISICA">Pessoa Física</SelectItem>
|
||||
</SelectContent>
|
||||
|
|
@ -290,7 +290,7 @@ export const BoletoCreationDialog = ({
|
|||
placeholder="0,00"
|
||||
value={formData.valor_servico}
|
||||
onChange={(e) => setFormData({...formData, valor_servico: e.target.value})}
|
||||
className="bg-blue-500/10 border-blue-500/30 text-white text-2xl font-bold h-14 pl-12 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20"
|
||||
className="bg-blue-50 dark:bg-blue-500/10 border-blue-200 dark:border-blue-500/30 text-blue-700 dark:text-white text-2xl font-bold h-14 pl-12 focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -298,11 +298,11 @@ export const BoletoCreationDialog = ({
|
|||
)}
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="p-6 pt-2 bg-slate-900/50 border-t border-slate-800 gap-3">
|
||||
<DialogFooter className="p-6 pt-2 bg-slate-50 dark:bg-slate-900/50 border-t border-slate-100 dark:border-slate-800 gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="text-slate-400 hover:text-white hover:bg-slate-800 h-10"
|
||||
className="text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800 h-10"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { parseCurrency, formatCurrency, formatCurrencyForInput } from '../utils/dateUtils';
|
||||
|
||||
/**
|
||||
* CurrencyInputV2 component for robust currency handling.
|
||||
* Displays formatted currency (R$ 0,00) when blurred.
|
||||
* Displays raw numeric format (0,00) when focused for easier editing.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {number|string} props.value - Numeric value
|
||||
* @param {Function} props.onChange - Callback with the numeric value
|
||||
* @param {string} [props.placeholder] - Input placeholder
|
||||
* @param {string} [props.className] - Additional CSS classes
|
||||
*/
|
||||
export const CurrencyInputV2 = ({ value, onChange, placeholder = "R$ 0,00", className, ...props }) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [localValue, setLocalValue] = useState('');
|
||||
|
||||
const isEmpty = value === '' || value == null;
|
||||
const displayValue = focused ? localValue : (isEmpty ? '' : formatCurrency(value));
|
||||
|
||||
const handleFocus = () => {
|
||||
setFocused(true);
|
||||
setLocalValue(isEmpty ? '' : formatCurrencyForInput(value));
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
const numericValue = parseCurrency(localValue);
|
||||
onChange(numericValue === '' ? '' : numericValue);
|
||||
setFocused(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
className={className}
|
||||
placeholder={placeholder}
|
||||
value={displayValue}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onChange={(e) => setLocalValue(e.target.value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -188,6 +188,17 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
}, [defaultView]);
|
||||
const [transactions, setTransactions] = useState([]);
|
||||
const [cruzamentoLoading, setCruzamentoLoading] = useState(true);
|
||||
const [clients, setClients] = useState([]);
|
||||
const [services, setServices] = useState([]);
|
||||
const [entradasPlanejadas, setEntradasPlanejadas] = useState([]);
|
||||
const [boletos, setBoletos] = useState(MOCK_BOLETOS || []);
|
||||
const [isLoadingServicos, setIsLoadingServicos] = useState(false);
|
||||
const [isLoadingClients, setIsLoadingClients] = useState(false);
|
||||
const [isLoadingEntradasPlanejadas, setIsLoadingEntradasPlanejadas] = useState(false);
|
||||
const [isLoadingItens, setIsLoadingItens] = useState(false);
|
||||
const [itensEntradaSelecionada, setItensEntradaSelecionada] = useState([]);
|
||||
const [categorias, setCategorias] = useState([]);
|
||||
const [caixas, setCaixas] = useState([]);
|
||||
|
||||
// Cruzamento: alimentado por /api/extrato/apresentar (tipoOperacao C). Planejado zerado.
|
||||
useEffect(() => {
|
||||
|
|
@ -243,19 +254,7 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
const [clients, setClients] = useState([]);
|
||||
const [services, setServices] = useState([]);
|
||||
const [entradasPlanejadas, setEntradasPlanejadas] = useState([]);
|
||||
const [boletos, setBoletos] = useState(MOCK_BOLETOS || []);
|
||||
const [isLoadingServicos, setIsLoadingServicos] = useState(false);
|
||||
const [isLoadingClients, setIsLoadingClients] = useState(false);
|
||||
const [isLoadingEntradasPlanejadas, setIsLoadingEntradasPlanejadas] = useState(false);
|
||||
const [isLoadingItens, setIsLoadingItens] = useState(false);
|
||||
const [itensEntradaSelecionada, setItensEntradaSelecionada] = useState([]);
|
||||
const [categorias, setCategorias] = useState([]);
|
||||
const [caixas, setCaixas] = useState([]);
|
||||
|
||||
/** Carrega serviços do backend (GET /servicos/list ou /servicos/list?tipo=...). Mantém mock quando VITE_USE_MOCK=true. */
|
||||
/** Carrega serviços do backend (GET /servicos/list ou /servicos/list?tipo=...). */
|
||||
const loadServicos = useCallback(async (tipo) => {
|
||||
setIsLoadingServicos(true);
|
||||
try {
|
||||
|
|
@ -271,10 +270,9 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
} finally {
|
||||
setIsLoadingServicos(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- toast omitido de propósito para evitar loop: callbacks estáveis
|
||||
}, []);
|
||||
}, [toast]);
|
||||
|
||||
/** Carrega clientes do backend (GET /empresas_financeiro). Mantém mock quando VITE_USE_MOCK=true. */
|
||||
/** Carrega clientes do backend (GET /empresas_financeiro). */
|
||||
const loadClients = useCallback(async () => {
|
||||
setIsLoadingClients(true);
|
||||
try {
|
||||
|
|
@ -287,8 +285,7 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
} finally {
|
||||
setIsLoadingClients(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- toast omitido de propósito para evitar loop: callbacks estáveis
|
||||
}, []);
|
||||
}, [toast]);
|
||||
|
||||
/** Carrega entradas planejadas do backend (GET /empresas_planejadas). */
|
||||
const loadEntradasPlanejadas = useCallback(async () => {
|
||||
|
|
@ -303,8 +300,7 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
} finally {
|
||||
setIsLoadingEntradasPlanejadas(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- toast omitido de propósito para evitar loop: callbacks estáveis
|
||||
}, []);
|
||||
}, [toast]);
|
||||
|
||||
// Executa uma única vez no mount; ref evita reexecução mesmo com Strict Mode ou dependências instáveis
|
||||
const didLoadReceitas = useRef(false);
|
||||
|
|
@ -320,7 +316,7 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
const totalRecebido = useMemo(() => {
|
||||
if (!Array.isArray(transactions)) return 0;
|
||||
return transactions
|
||||
.filter(t => t?.status === 'Recebido' || t?.status === 'Liquidado')
|
||||
.filter(t => t?.status === 'Recebido' || t?.status === 'Liquidado' || t?.status === 'Marcado como Recebido')
|
||||
.reduce((acc, t) => acc + (t?.valor || 0), 0);
|
||||
}, [transactions]);
|
||||
|
||||
|
|
@ -332,22 +328,22 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
|
||||
const totalGeral = totalRecebido + totalPendente;
|
||||
|
||||
// Actions para Serviços (API: /servicos/create, /servicos/edit, /servicos/delete)
|
||||
const parseValor = (v) => {
|
||||
// Actions para Serviços
|
||||
const parseValor = useCallback((v) => {
|
||||
if (v == null || v === '') return NaN;
|
||||
const n = typeof v === 'number' ? v : Number(String(v).replace(',', '.'));
|
||||
return Number.isFinite(n) ? n : NaN;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const formatDataEnvio = (dateStr) => {
|
||||
const formatDataEnvio = useCallback((dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
const y = d.getFullYear();
|
||||
const m = d.getMonth() + 1;
|
||||
const day = d.getDate();
|
||||
return `${y}-${m}-${day} `;
|
||||
};
|
||||
return `${y}-${m}-${day}`;
|
||||
}, []);
|
||||
|
||||
const createService = useCallback(async (serviceData) => {
|
||||
if (!serviceData.servico?.trim()) {
|
||||
|
|
@ -375,7 +371,7 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
toast.error('Erro ao criar serviço. Tente novamente.');
|
||||
return null;
|
||||
}
|
||||
}, [loadServicos, toast]);
|
||||
}, [loadServicos, toast, parseValor]);
|
||||
|
||||
const updateService = useCallback(async (id, serviceData) => {
|
||||
if (!serviceData.servico?.trim()) {
|
||||
|
|
@ -404,7 +400,7 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
toast.error('Erro ao editar serviço. Tente novamente.');
|
||||
return null;
|
||||
}
|
||||
}, [loadServicos, toast]);
|
||||
}, [loadServicos, toast, parseValor]);
|
||||
|
||||
const deleteService = useCallback(async (id) => {
|
||||
try {
|
||||
|
|
@ -420,8 +416,7 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
}, [loadServicos, toast]);
|
||||
|
||||
// Actions para Clientes
|
||||
const createClient = (clientData) => {
|
||||
// Validação de campos obrigatórios
|
||||
const createClient = useCallback(async (clientData) => {
|
||||
const camposObrigatorios = [];
|
||||
if (!clientData.nome?.trim()) camposObrigatorios.push('Nome');
|
||||
if (!clientData.email?.trim()) camposObrigatorios.push('Email');
|
||||
|
|
@ -432,17 +427,22 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const newClient = {
|
||||
id: clients.length + 1,
|
||||
...clientData
|
||||
};
|
||||
setClients(prev => [...prev, newClient]);
|
||||
toast.success('Cliente criado com sucesso!', 'Sucesso');
|
||||
return newClient;
|
||||
};
|
||||
setIsLoadingClients(true);
|
||||
try {
|
||||
const response = await workspaceReceitasService.createClient(clientData);
|
||||
await loadClients();
|
||||
toast.success('Cliente criado com sucesso!', 'Sucesso');
|
||||
return response;
|
||||
} catch (err) {
|
||||
console.warn('[useContasReceber] Erro ao criar cliente:', err);
|
||||
toast.handleBackendError(err);
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoadingClients(false);
|
||||
}
|
||||
}, [loadClients, toast]);
|
||||
|
||||
const updateClient = (id, clientData) => {
|
||||
// Validação de campos obrigatórios
|
||||
const updateClient = useCallback(async (id, clientData) => {
|
||||
const camposObrigatorios = [];
|
||||
if (!clientData.nome?.trim()) camposObrigatorios.push('Nome');
|
||||
if (!clientData.email?.trim()) camposObrigatorios.push('Email');
|
||||
|
|
@ -450,17 +450,45 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
|
||||
if (camposObrigatorios.length > 0) {
|
||||
toast.notifyFields(camposObrigatorios);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
setClients(prev => prev.map(c => c.id === id ? { ...c, ...clientData } : c));
|
||||
toast.success('Cliente atualizado com sucesso!', 'Sucesso');
|
||||
};
|
||||
setIsLoadingClients(true);
|
||||
try {
|
||||
await workspaceReceitasService.updateClient({ ...clientData, idempresa: id });
|
||||
await loadClients();
|
||||
toast.success('Cliente atualizado com sucesso!', 'Sucesso');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[useContasReceber] Erro ao atualizar cliente:', err);
|
||||
toast.handleBackendError(err);
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoadingClients(false);
|
||||
}
|
||||
}, [loadClients, toast]);
|
||||
|
||||
const deleteClient = (id) => {
|
||||
setClients(prev => prev.filter(c => c.id !== id));
|
||||
toast.success('Cliente excluído com sucesso!', 'Sucesso');
|
||||
};
|
||||
const updateClientStatus = useCallback(async (id, status) => {
|
||||
setIsLoadingClients(true);
|
||||
try {
|
||||
await workspaceReceitasService.updateClientStatus({ idempresa: id, status, data_envio: formatDataEnvio(new Date()) });
|
||||
await loadClients();
|
||||
toast.success(`Status do cliente alterado para ${status}!`, 'Sucesso');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[useContasReceber] Erro ao alterar status do cliente:', err);
|
||||
toast.handleBackendError(err);
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoadingClients(false);
|
||||
}
|
||||
}, [loadClients, toast, formatDataEnvio]);
|
||||
|
||||
const deleteClient = useCallback(async (id) => {
|
||||
// Nota: O service não tem deleteClient explicitamente, geralmente é feito via status Inativo
|
||||
// Mas podemos implementar se houver rota. Como não vi rota de delete no service, usarei updateStatus
|
||||
return updateClientStatus(id, 'Inativo');
|
||||
}, [updateClientStatus]);
|
||||
|
||||
const loadItensEntradaPlanejada = useCallback(async (idEntrada) => {
|
||||
if (!idEntrada) return;
|
||||
|
|
@ -480,8 +508,6 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
|
||||
// Actions para Entradas Planejadas
|
||||
const createEntradaPlanejada = useCallback(async (entradaData) => {
|
||||
|
||||
// Validação de campos obrigatórios
|
||||
const camposObrigatorios = [];
|
||||
if (!entradaData.dataEstimativa) camposObrigatorios.push('Data Estimativa');
|
||||
if (!entradaData.cliente?.trim()) camposObrigatorios.push('Cliente');
|
||||
|
|
@ -495,20 +521,13 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
|
||||
setIsLoadingEntradasPlanejadas(true);
|
||||
try {
|
||||
// Separar itens dos dados principais para envio sequencial
|
||||
const { itens, ...parentData } = entradaData;
|
||||
|
||||
// 1. Criar a entrada principal
|
||||
const response = await workspaceEntradasPlanejadasService.createEntradaPlanejada(parentData);
|
||||
|
||||
// Tenta obter o ID retornado pelo backend em diferentes formatos comuns
|
||||
let idEntrada = response?.id || response?.idempresa || response?.identradas_planejadas || response?.Base_Dados_API?.[0]?.id || (response?.Base_Dados_API && response.Base_Dados_API.id);
|
||||
|
||||
// FALLBACK: Se o backend não retornar o ID (apenas uma mensagem), buscamos na listagem
|
||||
if (!idEntrada) {
|
||||
console.log('[useContasReceber] ID não retornado, consultando listagem para capturar ID...');
|
||||
const list = await workspaceEntradasPlanejadasService.fetchEntradasPlanejadas();
|
||||
// Tenta encontrar a entrada que acabamos de criar (pelo número de referência e cliente)
|
||||
const matched = list.find(e =>
|
||||
String(e.numeroReferencia) === String(parentData.numeroReferencia) &&
|
||||
e.cliente === parentData.cliente
|
||||
|
|
@ -517,13 +536,12 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
}
|
||||
|
||||
if (!idEntrada) {
|
||||
console.warn('[useContasReceber] ID da entrada não encontrado após tentativa de consulta:', response);
|
||||
console.warn('[useContasReceber] ID da entrada não encontrado:', response);
|
||||
await loadEntradasPlanejadas();
|
||||
toast.error('Entrada criada, mas houve um problema ao vincular itens. Verifique o registro.');
|
||||
toast.error('Entrada criada, mas houve um problema ao vincular itens.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Criar os itens vinculados ao ID da entrada
|
||||
const itemPromises = itens.map(item =>
|
||||
workspaceEntradasPlanejadasService.createItemEntradaPlanejada({
|
||||
...item,
|
||||
|
|
@ -532,14 +550,12 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
);
|
||||
|
||||
await Promise.all(itemPromises);
|
||||
|
||||
|
||||
await loadEntradasPlanejadas();
|
||||
toast.success('Entrada e itens criados com sucesso!', 'Sucesso');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[useContasReceber] Erro ao criar entrada planejada:', err);
|
||||
toast.error('Erro ao criar entrada planejada. Tente novamente.');
|
||||
toast.error('Erro ao criar entrada planejada.');
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoadingEntradasPlanejadas(false);
|
||||
|
|
@ -547,7 +563,6 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
}, [loadEntradasPlanejadas, toast]);
|
||||
|
||||
const updateEntradaPlanejada = useCallback(async (id, entradaData) => {
|
||||
// Validação de campos obrigatórios
|
||||
const camposObrigatorios = [];
|
||||
if (!entradaData.dataEstimativa) camposObrigatorios.push('Data Estimativa');
|
||||
if (!entradaData.cliente?.trim()) camposObrigatorios.push('Cliente');
|
||||
|
|
@ -563,22 +578,16 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
try {
|
||||
const { itens, ...parentData } = entradaData;
|
||||
const idempresa = parentData.idempresa || id;
|
||||
|
||||
// 1. Atualizar a entrada principal
|
||||
await workspaceEntradasPlanejadasService.updateEntradaPlanejada(idempresa, parentData);
|
||||
|
||||
// 2. Processar itens (Update ou Create)
|
||||
const itemPromises = itens.map(item => {
|
||||
const idItem = item.identradas_planejadas_itens || item.id;
|
||||
|
||||
// Se o item já tem ID e não é um ID temporário, atualiza
|
||||
if (idItem && typeof idItem === 'number') {
|
||||
return workspaceEntradasPlanejadasService.updateItemEntradaPlanejada(idItem, {
|
||||
...item,
|
||||
idEntrada: idempresa
|
||||
});
|
||||
} else {
|
||||
// Se não tem ID, cria como novo item vinculado à entrada
|
||||
return workspaceEntradasPlanejadasService.createItemEntradaPlanejada({
|
||||
...item,
|
||||
idEntrada: idempresa
|
||||
|
|
@ -587,20 +596,18 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
});
|
||||
|
||||
await Promise.all(itemPromises);
|
||||
|
||||
await loadEntradasPlanejadas();
|
||||
toast.success('Entrada planejada atualizada com sucesso!', 'Sucesso');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[useContasReceber] Erro ao atualizar entrada planejada:', err);
|
||||
toast.error('Erro ao atualizar entrada planejada. Tente novamente.');
|
||||
toast.error('Erro ao atualizar entrada planejada.');
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoadingEntradasPlanejadas(false);
|
||||
}
|
||||
}, [loadEntradasPlanejadas, toast]);
|
||||
|
||||
|
||||
const deleteEntradaPlanejada = useCallback(async (id) => {
|
||||
try {
|
||||
await workspaceEntradasPlanejadasService.deleteEntradaPlanejada(id);
|
||||
|
|
@ -609,30 +616,27 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[useContasReceber] Erro ao excluir entrada planejada:', err);
|
||||
toast.error('Erro ao excluir entrada planejada. Tente novamente.');
|
||||
toast.error('Erro ao excluir entrada planejada.');
|
||||
return null;
|
||||
}
|
||||
}, [loadEntradasPlanejadas, toast]);
|
||||
|
||||
// Actions para Boletos
|
||||
const downloadBoleto = (boletoId) => {
|
||||
// Implementar download
|
||||
const downloadBoleto = useCallback((boletoId) => {
|
||||
console.log('Download boleto:', boletoId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const sendBoleto = (boletoId) => {
|
||||
// Implementar envio
|
||||
const sendBoleto = useCallback((boletoId) => {
|
||||
console.log('Enviar boleto:', boletoId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scheduleBoleto = (boletoId, newDate) => {
|
||||
const scheduleBoleto = useCallback((boletoId, newDate) => {
|
||||
setBoletos(prev => prev.map(b => b.id === boletoId ? { ...b, dataVencimento: newDate } : b));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const downloadFatura = (boletoId) => {
|
||||
// Implementar download fatura
|
||||
const downloadFatura = useCallback((boletoId) => {
|
||||
console.log('Download fatura:', boletoId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const actions = useMemo(() => ({
|
||||
setActiveSubView,
|
||||
|
|
@ -646,6 +650,7 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
// Clientes
|
||||
createClient,
|
||||
updateClient,
|
||||
updateClientStatus,
|
||||
deleteClient,
|
||||
// Entradas Planejadas
|
||||
createEntradaPlanejada,
|
||||
|
|
@ -658,11 +663,10 @@ export const useContasReceber = (defaultView = 'default') => {
|
|||
sendBoleto,
|
||||
scheduleBoleto,
|
||||
downloadFatura,
|
||||
|
||||
}), [
|
||||
setActiveSubView, loadServicos, loadClients, loadEntradasPlanejadas,
|
||||
createService, updateService, deleteService,
|
||||
createClient, updateClient, deleteClient,
|
||||
createClient, updateClient, updateClientStatus, deleteClient,
|
||||
createEntradaPlanejada, updateEntradaPlanejada, deleteEntradaPlanejada,
|
||||
loadItensEntradaPlanejada, setItensEntradaSelecionada,
|
||||
downloadBoleto, sendBoleto, scheduleBoleto, downloadFatura
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { extratoService } from '@/services/extratoService';
|
||||
import { formatDateForChart } from '../utils/dateUtils';
|
||||
import { boletosService } from '@/services/boletosService';
|
||||
import { workspaceDespesasService } from '@/services/workspaceDespesasService';
|
||||
import { workspaceSaldoService } from '@/services/workspaceSaldoService';
|
||||
|
|
@ -30,6 +31,11 @@ export const useDashboard = () => {
|
|||
boletosAbertos: []
|
||||
});
|
||||
|
||||
// Estado para filtros de gráfico
|
||||
const [jurosFilter, setJurosFilter] = useState('mensal'); // 'mensal' | 'anual'
|
||||
const [fluxoPeriodo, setFluxoPeriodo] = useState('0'); // 0 = atual, 1 = 1 mês atrás, etc.
|
||||
const [jurosData, setJurosData] = useState([]);
|
||||
|
||||
// Dados calculados para exibição
|
||||
const [resumo, setResumo] = useState({
|
||||
saldoTotal: 0,
|
||||
|
|
@ -98,7 +104,7 @@ export const useDashboard = () => {
|
|||
extratoService.fetchSaldo().catch(() => ({ disponivel: 0 })),
|
||||
extratoService.fetchSaldoArmazenado().catch(() => []),
|
||||
extratoService.fetchExtrato().catch(() => []),
|
||||
extratoService.fetchFluxo().catch(() => ({ mensal: [] })),
|
||||
extratoService.fetchFluxo().catch(() => ({ mensal: [], anual: [], diario: [] })),
|
||||
boletosService.fetchBoletosStatus().catch(() => ({ cobrancas: [] })),
|
||||
extratoService.fetchEmpresas().catch(() => []),
|
||||
workspaceDespesasService.fetchContasAPagar().catch(() => []),
|
||||
|
|
@ -249,38 +255,21 @@ export const useDashboard = () => {
|
|||
const diario = data.fluxo?.diario || [];
|
||||
const fluxoPorDia = {};
|
||||
const hoje = new Date();
|
||||
// Filtro: manter visualização do mês atual (como era antes)
|
||||
// ou talvez mostrar os últimos 30 dias se o usuário quiser histórico recente?
|
||||
// O título do card é "Histórico de Fluxo", e o subtítulo "Entradas vs Saídas".
|
||||
// A implementação original filtrava pelo "mesAtualStr". Vou manter para não quebrar a expectativa de "visão do mês".
|
||||
|
||||
// ATUALIZAÇÃO: Se o endpoint traz histórico, pode ser interessante mostrar os últimos X dias
|
||||
// se o mês estiver no começo. Mas para consistência com o resto do dashboard (que foca no mês),
|
||||
// vou filtrar pelo mês atual OU se o array estiver vazio, fallback.
|
||||
// Mas o pedido foi "alimentar o historico de fluxo como o fluxo diaria dessa rota".
|
||||
// A rota retorna MUITOS dias. O gráfico de barras vai ficar ilegível se mostrar tudo.
|
||||
// Vou filtrar pelo Mês Atual.
|
||||
|
||||
const mesAtualStr = formatMesAno(hoje);
|
||||
// Calcular qual mês mostrar com base no fluxoPeriodo
|
||||
const dataAlvo = new Date(hoje.getFullYear(), hoje.getMonth() - Number(fluxoPeriodo), 1);
|
||||
const mesAlvoStr = `${dataAlvo.getFullYear()}-${String(dataAlvo.getMonth() + 1).padStart(2, '0')}`;
|
||||
|
||||
diario.forEach((item) => {
|
||||
// O item tem structure: { data: "YYYY-MM-DD", tipoOperacao: "C"|"D", total: 123.45 }
|
||||
if (!item.data) return;
|
||||
|
||||
// item.data vem como "YYYY-MM-DD"
|
||||
// Vamos verificar se pertence ao mês atual
|
||||
// A string "YYYY-MM-DD" começa com o ano-mes.
|
||||
// formatMesAno retorna "YYYY-MM".
|
||||
|
||||
if (!item.data.startsWith(mesAtualStr)) return;
|
||||
// Filtrar pelo mês alvo
|
||||
if (!item.data.startsWith(mesAlvoStr)) return;
|
||||
|
||||
// item.data já está em YYYY-MM-DD.
|
||||
// Para o gráfico precisamos de DD/MM
|
||||
const [ano, mes, dia] = item.data.split('-');
|
||||
const diaFormatado = `${dia}/${mes}`;
|
||||
const diaFormatado = formatDateForChart(item.data);
|
||||
|
||||
if (!fluxoPorDia[diaFormatado]) {
|
||||
fluxoPorDia[diaFormatado] = { name: diaFormatado, entradas: 0, saidas: 0 };
|
||||
fluxoPorDia[diaFormatado] = { name: diaFormatado, entradas: 0, saidas: 0, rawDate: item.data };
|
||||
}
|
||||
|
||||
const valor = safeNumber(item.total);
|
||||
|
|
@ -291,18 +280,13 @@ export const useDashboard = () => {
|
|||
}
|
||||
});
|
||||
|
||||
// Converter para array e ordenar por data (dia)
|
||||
// Converter para array e ordenar cronologicamente pela data bruta
|
||||
return Object.values(fluxoPorDia)
|
||||
.sort((a, b) => {
|
||||
const [diaA, mesA] = a.name.split('/');
|
||||
const [diaB, mesB] = b.name.split('/');
|
||||
// Assumindo mesmo ano/mês, basta ordenar por dia
|
||||
return Number(diaA) - Number(diaB);
|
||||
});
|
||||
.sort((a, b) => a.rawDate.localeCompare(b.rawDate));
|
||||
};
|
||||
|
||||
const getBoletosPieData = () => {
|
||||
const counts = {};
|
||||
const values = {};
|
||||
const mesAtual = formatMesAno(new Date());
|
||||
|
||||
data.boletos.cobrancas?.forEach((item) => {
|
||||
|
|
@ -310,7 +294,8 @@ export const useDashboard = () => {
|
|||
if (formatMesAno(new Date(c.dataVencimento)) !== mesAtual) return;
|
||||
|
||||
const st = c.situacao || 'OUTROS';
|
||||
counts[st] = (counts[st] || 0) + 1;
|
||||
const valor = safeNumber(c.valorNominal);
|
||||
values[st] = (values[st] || 0) + valor;
|
||||
});
|
||||
|
||||
const colors = {
|
||||
|
|
@ -321,16 +306,14 @@ export const useDashboard = () => {
|
|||
'OUTROS': '#94a3b8'
|
||||
};
|
||||
|
||||
return Object.entries(counts).map(([name, value]) => ({
|
||||
return Object.entries(values).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
color: colors[name] || colors['OUTROS']
|
||||
}));
|
||||
};
|
||||
|
||||
// Estado para filtro do gráfico de juros
|
||||
const [jurosFilter, setJurosFilter] = useState('mensal'); // 'mensal' | 'anual'
|
||||
const [jurosData, setJurosData] = useState([]);
|
||||
// ... (previous code)
|
||||
|
||||
// ... (previous code)
|
||||
|
||||
|
|
@ -423,6 +406,8 @@ export const useDashboard = () => {
|
|||
getJurosData,
|
||||
jurosFilter,
|
||||
setJurosFilter,
|
||||
fluxoPeriodo,
|
||||
setFluxoPeriodo,
|
||||
getUltimasTransacoes,
|
||||
fetchEntradasMes: async (mes, ano) => {
|
||||
try {
|
||||
|
|
@ -469,6 +454,14 @@ export const useDashboard = () => {
|
|||
return [];
|
||||
}
|
||||
},
|
||||
fetchJurosAdicionados: async (mes, ano) => {
|
||||
try {
|
||||
return await extratoService.fetchJurosAdicionados({ mes, ano });
|
||||
} catch (err) {
|
||||
console.error('Error fetching juros adicionados:', err);
|
||||
return { dados: [], total_adicionado: 0, total_registros: 0 };
|
||||
}
|
||||
},
|
||||
reload: loadData
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const useFornecedores = () => {
|
|||
servico: item.servico || '',
|
||||
valor: item.valor || 0,
|
||||
obs: item.obs || '',
|
||||
idregra: item.idregra || item.regra_id || null,
|
||||
idregra: Array.isArray(item.idregra) ? item.idregra : (item.idregra ? [item.idregra] : (item.regra_id ? [item.regra_id] : [])),
|
||||
regra_id: item.idregra || item.regra_id || null,
|
||||
nome_regra: item.nome_regra || null,
|
||||
// Campos opcionais para compatibilidade
|
||||
|
|
@ -108,7 +108,7 @@ export const useFornecedores = () => {
|
|||
servico: newFornecedor.servico || fornecedorData.servico || '',
|
||||
valor: newFornecedor.valor || fornecedorData.valor || 0,
|
||||
obs: newFornecedor.obs || fornecedorData.obs || '',
|
||||
idregra: newFornecedor.idregra || newFornecedor.regra_id || fornecedorData.idregra || fornecedorData.regra_id || null,
|
||||
idregra: Array.isArray(newFornecedor.idregra) ? newFornecedor.idregra : (newFornecedor.idregra ? [newFornecedor.idregra] : (fornecedorData.idregra ? (Array.isArray(fornecedorData.idregra) ? fornecedorData.idregra : [fornecedorData.idregra]) : [])),
|
||||
regra_id: newFornecedor.idregra || newFornecedor.regra_id || fornecedorData.idregra || fornecedorData.regra_id || null,
|
||||
status: 'Ativo',
|
||||
moeda_padrao: 'BRL',
|
||||
|
|
@ -170,7 +170,7 @@ export const useFornecedores = () => {
|
|||
servico: updated.servico || fornecedorData.servico || '',
|
||||
valor: updated.valor || fornecedorData.valor || 0,
|
||||
obs: updated.obs || fornecedorData.obs || '',
|
||||
idregra: updated.idregra || updated.regra_id || fornecedorData.idregra || fornecedorData.regra_id || null,
|
||||
idregra: Array.isArray(updated.idregra) ? updated.idregra : (updated.idregra ? [updated.idregra] : (fornecedorData.idregra ? (Array.isArray(fornecedorData.idregra) ? fornecedorData.idregra : [fornecedorData.idregra]) : [])),
|
||||
regra_id: updated.idregra || updated.regra_id || fornecedorData.idregra || fornecedorData.regra_id || null,
|
||||
// Preserva campos existentes
|
||||
status: fornecedorData.status || 'Ativo',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
|
|
@ -5,77 +6,56 @@ import { toast } from 'sonner';
|
|||
* Usa sonner para exibir mensagens de sucesso, erro, warning e info
|
||||
*/
|
||||
export const useToast = () => {
|
||||
return {
|
||||
/**
|
||||
* Exibe uma notificação de sucesso
|
||||
* @param {string} message - Mensagem a ser exibida
|
||||
* @param {string} title - Título opcional
|
||||
*/
|
||||
success: (message, title = 'Sucesso!') => {
|
||||
toast.success(title, {
|
||||
description: message,
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
const success = useCallback((message, title = 'Sucesso!') => {
|
||||
toast.success(title, {
|
||||
description: message,
|
||||
duration: 4000,
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Exibe uma notificação de erro
|
||||
* @param {string} message - Mensagem a ser exibida
|
||||
* @param {string} title - Título opcional
|
||||
*/
|
||||
error: (message, title = 'Erro!') => {
|
||||
toast.error(title, {
|
||||
description: message,
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
const error = useCallback((message, title = 'Erro!') => {
|
||||
toast.error(title, {
|
||||
description: message,
|
||||
duration: 5000,
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Exibe uma notificação de aviso
|
||||
* @param {string} message - Mensagem a ser exibida
|
||||
* @param {string} title - Título opcional
|
||||
*/
|
||||
warning: (message, title = 'Atenção!') => {
|
||||
toast.warning(title, {
|
||||
description: message,
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
const warning = useCallback((message, title = 'Atenção!') => {
|
||||
toast.warning(title, {
|
||||
description: message,
|
||||
duration: 4000,
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Exibe uma notificação informativa
|
||||
* @param {string} message - Mensagem a ser exibida
|
||||
* @param {string} title - Título opcional
|
||||
*/
|
||||
info: (message, title = 'Informação') => {
|
||||
toast.info(title, {
|
||||
description: message,
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
const info = useCallback((message, title = 'Informação') => {
|
||||
toast.info(title, {
|
||||
description: message,
|
||||
duration: 4000,
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Exibe uma notificação de campos obrigatórios não preenchidos
|
||||
* @param {string[]} fields - Lista de campos obrigatórios
|
||||
*/
|
||||
notifyFields: (fields) => {
|
||||
toast.warning('Campos Obrigatórios', {
|
||||
description: `Por favor, preencha: ${fields.join(', ')}`,
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
const notifyFields = useCallback((fields) => {
|
||||
toast.warning('Campos Obrigatórios', {
|
||||
description: `Por favor, preencha: ${fields.join(', ')}`,
|
||||
duration: 5000,
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Trata erros de backend de forma amigável
|
||||
* @param {Error} error - Erro capturado
|
||||
*/
|
||||
handleBackendError: (error) => {
|
||||
console.error(error);
|
||||
const msg = error.response?.data?.message || error.message || 'Erro desconhecido na comunicação com o servidor.';
|
||||
toast.error('Erro do Sistema', {
|
||||
description: msg,
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
};
|
||||
const handleBackendError = useCallback((error) => {
|
||||
console.error(error);
|
||||
const msg = error.response?.data?.message || error.message || 'Erro desconhecido na comunicação com o servidor.';
|
||||
toast.error('Erro do Sistema', {
|
||||
description: msg,
|
||||
duration: 5000,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return useMemo(() => ({
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
notifyFields,
|
||||
handleBackendError,
|
||||
}), [success, error, warning, info, notifyFields, handleBackendError]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ export const formatDate = (dateString) => {
|
|||
const [year, month, day] = dateString.split('-').map(part => part.split('T')[0]);
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
// Se a string já está no formato DD/MM/YYYY, retornar como está
|
||||
if (typeof dateString === 'string' && /^\d{2}\/\d{2}\/\d{4}/.test(dateString)) {
|
||||
return dateString;
|
||||
}
|
||||
|
||||
// Se for objeto Date
|
||||
if (dateString instanceof Date) {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@ import { FinanceiroChartTooltip } from '../components/FinanceiroChartTooltip';
|
|||
import { useConciliacaoV2 } from '../hooks/useConciliacaoV2';
|
||||
import { ExtratoCompletoView } from './conciliacao-v2/ExtratoCompletoView';
|
||||
import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
|
||||
|
||||
const MESES_REC = ['', 'Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
||||
const ANOS_REC = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() - 5 + i);
|
||||
|
||||
// Componente de card de resumo premium
|
||||
const XPICard = ({
|
||||
|
|
@ -135,11 +138,14 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
|
|||
getJurosData,
|
||||
jurosFilter,
|
||||
setJurosFilter,
|
||||
fluxoPeriodo,
|
||||
setFluxoPeriodo,
|
||||
getUltimasTransacoes,
|
||||
fetchEntradasMes,
|
||||
fetchSaidasPeriodo,
|
||||
fetchTransacoesNaoConciliadas,
|
||||
fetchBoletosAbertos,
|
||||
fetchJurosAdicionados,
|
||||
reload
|
||||
} = useDashboard();
|
||||
|
||||
|
|
@ -150,56 +156,88 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
|
|||
saldo: false,
|
||||
despesas: false,
|
||||
naoConciliadas: false,
|
||||
boletosAbertos: false
|
||||
boletosAbertos: false,
|
||||
adicionado: false
|
||||
});
|
||||
const [modalData, setModalData] = useState({
|
||||
saldo: [],
|
||||
despesas: [],
|
||||
naoConciliadas: [],
|
||||
boletosAbertos: []
|
||||
boletosAbertos: [],
|
||||
adicionado: {
|
||||
lista: [],
|
||||
total_adicionado: 0,
|
||||
total_registros: 0
|
||||
}
|
||||
});
|
||||
const [modalLoading, setModalLoading] = useState(false);
|
||||
const [modalDate, setModalDate] = useState(new Date());
|
||||
|
||||
// New states for juros detailing period selectors
|
||||
const [jurosDetailPeriodo, setJurosDetailPeriodo] = useState('mes'); // 'mes' | 'ano'
|
||||
const [jurosDetailMes, setJurosDetailMes] = useState(String(new Date().getMonth() + 1));
|
||||
const [jurosDetailAno, setJurosDetailAno] = useState(String(new Date().getFullYear()));
|
||||
|
||||
// New states for requested adjustments
|
||||
// New states for requested adjustments
|
||||
const [isExtratoPopupOpen, setIsExtratoPopupOpen] = useState(false);
|
||||
const [isCategorizacaoOpen, setIsCategorizacaoOpen] = useState(false);
|
||||
|
||||
const fetchDataForModal = async (type, date) => {
|
||||
const mes = date.getMonth() + 1;
|
||||
const ano = date.getFullYear();
|
||||
let data = [];
|
||||
const fetchDataForModal = async (type, dateOrParams) => {
|
||||
let mes, ano;
|
||||
|
||||
if (type === 'adicionado' && typeof dateOrParams === 'object') {
|
||||
mes = dateOrParams.mes;
|
||||
ano = dateOrParams.ano;
|
||||
} else {
|
||||
const d = dateOrParams instanceof Date ? dateOrParams : new Date();
|
||||
mes = d.getMonth() + 1;
|
||||
ano = d.getFullYear();
|
||||
}
|
||||
|
||||
try {
|
||||
setModalLoading(true);
|
||||
switch (type) {
|
||||
case 'saldo':
|
||||
data = await fetchEntradasMes(mes, ano);
|
||||
break;
|
||||
case 'despesas':
|
||||
data = await fetchSaidasPeriodo(mes, ano);
|
||||
break;
|
||||
case 'naoConciliadas':
|
||||
data = await fetchTransacoesNaoConciliadas(); // Usually global, but could filter if APIs allowed
|
||||
break;
|
||||
case 'boletosAbertos':
|
||||
data = await fetchBoletosAbertos(); // Global
|
||||
break;
|
||||
}
|
||||
setModalData(prev => ({ ...prev, [type]: data }));
|
||||
} catch (err) {
|
||||
console.error(`Error loading modal data for ${type}:`, err);
|
||||
} finally {
|
||||
setModalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = (type) => {
|
||||
setModalState(prev => ({ ...prev, [type]: true }));
|
||||
const now = new Date();
|
||||
setModalDate(now); // Reset to current month on open
|
||||
fetchDataForModal(type, now);
|
||||
};
|
||||
let data = [];
|
||||
|
||||
try {
|
||||
setModalLoading(true);
|
||||
switch (type) {
|
||||
case 'saldo':
|
||||
data = await fetchEntradasMes(mes, ano);
|
||||
break;
|
||||
case 'despesas':
|
||||
data = await fetchSaidasPeriodo(mes, ano);
|
||||
break;
|
||||
case 'naoConciliadas':
|
||||
data = await fetchTransacoesNaoConciliadas(); // Usually global, but could filter if APIs allowed
|
||||
break;
|
||||
case 'boletosAbertos':
|
||||
data = await fetchBoletosAbertos(); // Global
|
||||
break;
|
||||
case 'adicionado':
|
||||
const result = await fetchJurosAdicionados(mes, ano);
|
||||
data = {
|
||||
lista: result.dados || result.adicionado || [],
|
||||
total_adicionado: result.total_adicionado || 0,
|
||||
total_registros: result.total_registros || 0
|
||||
};
|
||||
break;
|
||||
}
|
||||
setModalData(prev => ({ ...prev, [type]: data }));
|
||||
} catch (err) {
|
||||
console.error(`Error loading modal data for ${type}:`, err);
|
||||
} finally {
|
||||
setModalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = (type) => {
|
||||
setModalState(prev => ({ ...prev, [type]: true }));
|
||||
const now = new Date();
|
||||
if (type === 'adicionado') {
|
||||
fetchDataForModal(type, { mes: jurosDetailMes, ano: jurosDetailAno });
|
||||
} else {
|
||||
setModalDate(now);
|
||||
fetchDataForModal(type, now);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModalDateChange = (type, newDate) => {
|
||||
fetchDataForModal(type, newDate);
|
||||
|
|
@ -252,7 +290,7 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
|
|||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white tracking-tight">Painel Financeiro</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 uppercase tracking-widest font-bold mt-1">Visão Geral do Workspace</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* <div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-white dark:bg-[#0f172a] border-slate-200 dark:border-slate-800 text-slate-900 dark:text-white hover:bg-slate-50 dark:hover:bg-slate-800 h-9 px-4 text-xs font-bold"
|
||||
|
|
@ -268,7 +306,7 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
|
|||
<DollarSign className="w-3.5 h-3.5 mr-2" />
|
||||
Nova Receita
|
||||
</Button>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* KPI Grid */}
|
||||
|
|
@ -356,6 +394,13 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
|
|||
<div className="p-2 bg-blue-500/10 rounded-lg">
|
||||
<TrendingUp className="w-4 h-4 text-blue-500" />
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 px-3 text-[10px] font-bold uppercase tracking-wider border-slate-200 dark:border-slate-700 bg-white dark:bg-[#0f172a] hover:bg-slate-50 dark:hover:bg-slate-800 text-slate-900 dark:text-white"
|
||||
onClick={() => openModal('adicionado')}
|
||||
>
|
||||
Detalhamento
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -462,7 +507,20 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
|
|||
<h3 className="text-lg font-bold text-slate-900 dark:text-white tracking-tight">Histórico de Fluxo</h3>
|
||||
<p className="text-[10px] font-bold text-slate-500 dark:text-slate-500 uppercase tracking-widest">Entradas vs Saídas</p>
|
||||
</div>
|
||||
<Calendar className="w-4 h-4 text-slate-500" />
|
||||
<div className="flex items-center gap-3">
|
||||
<Select value={fluxoPeriodo} onValueChange={setFluxoPeriodo}>
|
||||
<SelectTrigger className="w-[140px] h-8 text-xs font-bold uppercase tracking-wider bg-slate-50 dark:bg-slate-900 border-slate-200 dark:border-slate-700">
|
||||
<SelectValue placeholder="Período" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[10000]">
|
||||
<SelectItem value="0" className="text-xs font-bold uppercase">Mês Vigente</SelectItem>
|
||||
<SelectItem value="1" className="text-xs font-bold uppercase">1 Mês Atrás</SelectItem>
|
||||
<SelectItem value="2" className="text-xs font-bold uppercase">2 Meses Atrás</SelectItem>
|
||||
<SelectItem value="3" className="text-xs font-bold uppercase">3 Meses Atrás</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Calendar className="w-4 h-4 text-slate-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[280px]">
|
||||
|
|
@ -885,13 +943,116 @@ import { CategorizacaoDialog } from '../components/CategorizacaoDialog';
|
|||
{formatCurrency(modalData.boletosAbertos.reduce((acc, curr) => acc + (Number(curr.cobranca?.valorNominal || curr.valorNominal || 0)), 0))}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="outline"
|
||||
className="border-slate-200 text-slate-700 h-10 px-6 font-bold uppercase text-[10px]"
|
||||
onClick={() => onNavigate && onNavigate('receber/boletos-pendentes')}
|
||||
>
|
||||
Gerenciar Todos os Boletos
|
||||
</Button>
|
||||
</Button> */}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Modal - Detalhamento de Juros Adicionados */}
|
||||
<Dialog open={modalState.adicionado} onOpenChange={(open) => setModalState(prev => ({ ...prev, adicionado: open }))}>
|
||||
<DialogContent className="max-w-5xl bg-white dark:bg-[#0f172a] border-slate-200 dark:border-slate-800 z-[9999] flex flex-col max-h-[90vh]">
|
||||
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<div className="space-y-1">
|
||||
<DialogTitle className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<TrendingUp className="text-blue-500" />
|
||||
Juros Adicionados - {jurosDetailPeriodo === 'mes' ? `${MESES_REC[Number(jurosDetailMes)]} / ${jurosDetailAno}` : jurosDetailAno}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
Detalhamento dos juros e acréscimos identificados no período.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-widest hidden sm:block">Período:</span>
|
||||
|
||||
<Select value={jurosDetailPeriodo} onValueChange={(val) => {
|
||||
setJurosDetailPeriodo(val);
|
||||
fetchDataForModal('adicionado', { mes: val === 'ano' ? null : jurosDetailMes, ano: jurosDetailAno });
|
||||
}}>
|
||||
<SelectTrigger className="h-8 w-[100px] bg-white dark:bg-slate-900/50 border-slate-200 dark:border-slate-800 text-xs font-semibold">
|
||||
<SelectValue placeholder="Tipo" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[10001]">
|
||||
<SelectItem value="mes">Mensal</SelectItem>
|
||||
<SelectItem value="ano">Anual</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{jurosDetailPeriodo === 'mes' && (
|
||||
<Select value={jurosDetailMes} onValueChange={(val) => {
|
||||
setJurosDetailMes(val);
|
||||
fetchDataForModal('adicionado', { mes: val, ano: jurosDetailAno });
|
||||
}}>
|
||||
<SelectTrigger className="h-8 w-[120px] bg-white dark:bg-slate-900/50 border-slate-200 dark:border-slate-800 text-xs font-semibold">
|
||||
<SelectValue placeholder="Mês" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[10001]">
|
||||
{MESES_REC.map((m, i) => (i === 0 ? null : <SelectItem key={i} value={String(i)}>{m}</SelectItem>))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<Select value={jurosDetailAno} onValueChange={(val) => {
|
||||
setJurosDetailAno(val);
|
||||
fetchDataForModal('adicionado', { mes: jurosDetailPeriodo === 'ano' ? null : jurosDetailMes, ano: val });
|
||||
}}>
|
||||
<SelectTrigger className="h-8 w-[90px] bg-white dark:bg-slate-900/50 border-slate-200 dark:border-slate-800 text-xs font-semibold">
|
||||
<SelectValue placeholder="Ano" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[10001]">
|
||||
{ANOS_REC.map((a) => <SelectItem key={a} value={String(a)}>{a}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 h-[clamp(300px,60vh,600px)] overflow-auto custom-scrollbar">
|
||||
<ExcelTable
|
||||
data={modalData.adicionado.lista}
|
||||
loading={modalLoading}
|
||||
columns={[
|
||||
{ header: 'Data', field: 'dataEntrada', width: 120, render: (row) => (
|
||||
<span className="text-slate-500 font-medium">{formatDate(row.dataEntrada)}</span>
|
||||
)},
|
||||
{ header: 'Beneficiário/Pagador', field: 'beneficiario_pagador', width: 250, render: (row) => (
|
||||
<span className="font-bold text-slate-900 dark:text-white">{row.beneficiario_pagador || 'N/A'}</span>
|
||||
)},
|
||||
{ header: 'Descrição', field: 'descricao', width: 300 },
|
||||
{ header: 'Tipo', field: 'tipoTransacao', width: 120, render: (row) => (
|
||||
<Badge variant="outline" className="text-[10px] font-bold uppercase tracking-widest bg-slate-100 dark:bg-slate-800 text-slate-500">
|
||||
{row.tipoTransacao || 'GERAL'}
|
||||
</Badge>
|
||||
)},
|
||||
{ header: 'Valor (Juros)', field: 'adicionado', width: 120, render: (row) => (
|
||||
<span className="font-bold text-blue-600 dark:text-blue-400">{formatCurrency(row.adicionado)}</span>
|
||||
)},
|
||||
{ header: 'Valor Total', field: 'valor', width: 120, render: (row) => (
|
||||
<span className="text-slate-500">{formatCurrency(row.valor)}</span>
|
||||
)}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="bg-slate-50 dark:bg-slate-900/40 p-4 -mx-6 -mb-6 rounded-b-xl border-t border-slate-200 dark:border-slate-800 flex justify-between shrink-0">
|
||||
<div className="text-left">
|
||||
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Total de Juros Adicionados</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{formatCurrency(modalData.adicionado.total_adicionado)}
|
||||
</p>
|
||||
<span className="text-xs text-slate-400 font-medium">({modalData.adicionado.total_registros} registros)</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* <Button
|
||||
variant="outline"
|
||||
className="border-slate-200 text-slate-700 h-10 px-6 font-bold uppercase text-[10px]"
|
||||
onClick={() => setModalState(prev => ({ ...prev, adicionado: false }))}
|
||||
>
|
||||
Fechar
|
||||
</Button> */}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ export const ConciliacaoView = ({ initialView }) => {
|
|||
<div className="flex items-center justify-between gap-4 mb-4 sm:mb-6">
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-white tracking-tight flex items-center gap-2 sm:gap-3">
|
||||
Conciliação
|
||||
<Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-[10px] hidden sm:inline-flex">PREMIUM v2</Badge>
|
||||
{/* <Badge className="bg-blue-500/10 text-blue-500 border-blue-500/20 text-[10px] hidden sm:inline-flex">PREMIUM v2</Badge> */}
|
||||
</h1>
|
||||
|
||||
{/* <Button
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export const ContasPagarView = () => {
|
|||
<div className="flex items-center justify-between gap-4 mb-4 sm:mb-6">
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-white tracking-tight flex items-center gap-2 sm:gap-3">
|
||||
Contas a Pagar
|
||||
<Badge className="bg-rose-500/10 text-rose-500 border-rose-500/20 text-[10px] hidden sm:inline-flex">PREMIUM v2</Badge>
|
||||
{/* <Badge className="bg-rose-500/10 text-rose-500 border-rose-500/20 text-[10px] hidden sm:inline-flex">PREMIUM v2</Badge> */}
|
||||
</h1>
|
||||
|
||||
{/* <Button
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import { formatCurrency, parseDateInfo, parseCurrency } from '@/utils/dateUtils'
|
|||
/**
|
||||
* Premium KPI Card com visualização clara
|
||||
*/
|
||||
const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, percentual }) => {
|
||||
const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, percentual, loading }) => {
|
||||
const Icon = icon;
|
||||
return (
|
||||
<Card className="bg-white dark:bg-slate-900/50 border-2 shadow-lg rounded-xl overflow-hidden group hover:scale-[1.02] transition-all">
|
||||
|
|
@ -52,12 +52,16 @@ const KPICard = ({ title, value, subtext, icon, colorClass, highlight, trend, pe
|
|||
<div className="flex items-start justify-between mb-[clamp(0.5rem,0.6vw,0.625rem)]">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[clamp(0.625rem,0.7vw,0.75rem)] font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-[0.2em] mb-[clamp(0.25rem,0.4vw,0.375rem)]">{title}</p>
|
||||
<h3 className={cn(
|
||||
"text-[clamp(1.5rem,2vw,1.875rem)] font-semibold tracking-tight mb-[clamp(0.25rem,0.4vw,0.375rem)]",
|
||||
highlight ? 'text-emerald-400 dark:text-emerald-400' : 'text-slate-900 dark:text-white'
|
||||
)}>{value}</h3>
|
||||
{loading ? (
|
||||
<div className="h-[clamp(1.5rem,2vw,1.875rem)] w-32 bg-slate-200 dark:bg-slate-700 animate-pulse rounded mt-1" />
|
||||
) : (
|
||||
<h3 className={cn(
|
||||
"text-[clamp(1.5rem,2vw,1.875rem)] font-semibold tracking-tight mb-[clamp(0.25rem,0.4vw,0.375rem)]",
|
||||
highlight ? 'text-rose-400 dark:text-rose-400' : 'text-slate-900 dark:text-white'
|
||||
)}>{value}</h3>
|
||||
)}
|
||||
<div className="flex items-center gap-[clamp(0.375rem,0.5vw,0.5rem)] mt-[clamp(0.375rem,0.5vw,0.5rem)] flex-wrap">
|
||||
{trend && (
|
||||
{trend && !loading && (
|
||||
<span className={cn(
|
||||
"text-[clamp(0.625rem,0.7vw,0.75rem)] font-semibold px-[clamp(0.375rem,0.4vw,0.5rem)] py-0.5 rounded shrink-0 flex items-center gap-[clamp(0.25rem,0.3vw,0.375rem)]",
|
||||
trend === 'up' ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400' :
|
||||
|
|
@ -97,6 +101,70 @@ export const CruzamentoDespesasView = ({ state }) => {
|
|||
const [somaCategorias, setSomaCategorias] = React.useState([]);
|
||||
const [isLoadingSoma, setIsLoadingSoma] = React.useState(false);
|
||||
|
||||
const [chartDataBackend, setChartDataBackend] = React.useState([]);
|
||||
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
|
||||
|
||||
const [totaisDespesas, setTotaisDespesas] = React.useState({
|
||||
total_planejado: 0,
|
||||
total_executado: 0,
|
||||
total_geral: 0,
|
||||
diferenca: 0,
|
||||
taxa_realizacao: 0,
|
||||
classificacao: '',
|
||||
cor: ''
|
||||
});
|
||||
const [isLoadingTotais, setIsLoadingTotais] = React.useState(false);
|
||||
|
||||
// Efeito para buscar totais planejados do mês vigente
|
||||
React.useEffect(() => {
|
||||
const fetchTotais = async () => {
|
||||
if (filtroTipo !== 'mes') return;
|
||||
|
||||
setIsLoadingTotais(true);
|
||||
try {
|
||||
const result = await extratoService.fetchDespesasResumo({
|
||||
mes: filtroMes,
|
||||
ano: filtroAno
|
||||
});
|
||||
|
||||
if (result) {
|
||||
setTotaisDespesas(result);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CruzamentoDespesasView] Erro ao buscar totais:', err);
|
||||
} finally {
|
||||
setIsLoadingTotais(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTotais();
|
||||
}, [filtroMes, filtroAno, filtroTipo]);
|
||||
|
||||
// Efeito para buscar dados do gráfico do backend
|
||||
React.useEffect(() => {
|
||||
const fetchChartData = async () => {
|
||||
setIsLoadingChart(true);
|
||||
try {
|
||||
const params = {
|
||||
ano: filtroAno
|
||||
};
|
||||
if (filtroTipo === 'mes') {
|
||||
params.mes = filtroMes;
|
||||
}
|
||||
const result = await extratoService.fetchDespesasGrafico(params);
|
||||
const data = result?.grafico ?? result ?? [];
|
||||
setChartDataBackend(Array.isArray(data) ? data : []);
|
||||
} catch (err) {
|
||||
console.error('[CruzamentoDespesasView] Erro ao buscar dados do gráfico:', err);
|
||||
setChartDataBackend([]);
|
||||
} finally {
|
||||
setIsLoadingChart(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChartData();
|
||||
}, [filtroMes, filtroAno, filtroTipo]);
|
||||
|
||||
// Efeito para buscar soma por categoria do backend
|
||||
React.useEffect(() => {
|
||||
const fetchSoma = async () => {
|
||||
|
|
@ -180,6 +248,31 @@ export const CruzamentoDespesasView = ({ state }) => {
|
|||
}, [somaCategorias]);
|
||||
|
||||
const timelineData = useMemo(() => {
|
||||
if (chartDataBackend.length > 0) {
|
||||
return chartDataBackend.map(item => {
|
||||
const variacao = (item.executado || 0) - (item.planejado || 0);
|
||||
const percentual = (item.planejado || 0) > 0 ? (((item.executado || 0) / item.planejado) * 100).toFixed(1) : 0;
|
||||
|
||||
// Formata o nome do período para exibição
|
||||
let name = String(item.periodo);
|
||||
if (filtroTipo === 'ano') {
|
||||
const mesesLabel = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
||||
const mesIdx = parseInt(item.periodo) - 1;
|
||||
if (mesIdx >= 0 && mesIdx < 12) name = mesesLabel[mesIdx];
|
||||
} else if (filtroTipo === 'mes') {
|
||||
name = `${String(item.periodo).padStart(2, '0')}/${String(filtroMes).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
name,
|
||||
variacao,
|
||||
percentual,
|
||||
value: item.executado
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const mesesLabel = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
||||
|
||||
if (filtroTipo === 'ano') {
|
||||
|
|
@ -237,7 +330,7 @@ export const CruzamentoDespesasView = ({ state }) => {
|
|||
return Object.entries(map)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([key, values]) => ({ ...values, value: values.executado }));
|
||||
}, [filteredPlanejadas, filteredExecutadas, filtroTipo, filtroAno]);
|
||||
}, [filteredPlanejadas, filteredExecutadas, filtroTipo, filtroAno, chartDataBackend, filtroMes]);
|
||||
|
||||
const categoriaData = useMemo(() => {
|
||||
return comparativoCategoria.map((item) => ({
|
||||
|
|
@ -247,12 +340,40 @@ export const CruzamentoDespesasView = ({ state }) => {
|
|||
}, [comparativoCategoria]);
|
||||
|
||||
const kpisCruzamento = useMemo(() => {
|
||||
if (filtroTipo === 'mes' && totaisDespesas) {
|
||||
const totalPlanejado = totaisDespesas.total_planejado || 0;
|
||||
const totalExecutado = totaisDespesas.total_executado || 0;
|
||||
const variacao = totaisDespesas.diferenca || 0;
|
||||
const percentualVariacao = totaisDespesas.taxa_realizacao || 0;
|
||||
const totalVolume = totaisDespesas.total_geral || (totalPlanejado + totalExecutado);
|
||||
const classificacao = totaisDespesas.classificacao || '';
|
||||
|
||||
// Mapeamento de cores do back para Tailwind
|
||||
const colorMap = {
|
||||
'verde': 'text-emerald-500',
|
||||
'azul': 'text-blue-500',
|
||||
'amarelo': 'text-amber-500',
|
||||
'vermelho': 'text-rose-500'
|
||||
};
|
||||
const corDinamica = colorMap[totaisDespesas.cor] || 'text-blue-500';
|
||||
|
||||
return { totalPlanejado, totalExecutado, variacao, percentualVariacao, totalVolume, classificacao, corDinamica };
|
||||
}
|
||||
|
||||
const totalPlanejado = filteredPlanejadas.reduce((acc, item) => acc + (item.valor || 0), 0);
|
||||
const totalExecutado = filteredExecutadas.reduce((acc, item) => acc + (item.valor || 0), 0);
|
||||
const variacao = totalExecutado - totalPlanejado;
|
||||
const percentualVariacao = totalPlanejado > 0 ? ((totalExecutado / totalPlanejado) * 100).toFixed(1) : 0;
|
||||
return { totalPlanejado, totalExecutado, variacao, percentualVariacao };
|
||||
}, [filteredPlanejadas, filteredExecutadas]);
|
||||
return {
|
||||
totalPlanejado,
|
||||
totalExecutado,
|
||||
variacao,
|
||||
percentualVariacao,
|
||||
totalVolume: totalPlanejado + totalExecutado,
|
||||
classificacao: '',
|
||||
corDinamica: 'text-blue-500'
|
||||
};
|
||||
}, [filteredPlanejadas, filteredExecutadas, filtroTipo, totaisDespesas]);
|
||||
|
||||
const COLORS = ['#ef4444', '#f97316', '#f59e0b', '#8b5cf6', '#64748b', '#10b981'];
|
||||
|
||||
|
|
@ -260,8 +381,10 @@ export const CruzamentoDespesasView = ({ state }) => {
|
|||
const totalExecutado = kpisCruzamento.totalExecutado || 0;
|
||||
const variacao = kpisCruzamento.variacao || 0;
|
||||
const percentualVariacao = kpisCruzamento.percentualVariacao ?? 0;
|
||||
const totalVolume = kpisCruzamento.totalVolume || 0;
|
||||
const classificacao = kpisCruzamento.classificacao;
|
||||
const corDinamica = kpisCruzamento.corDinamica;
|
||||
const temPlanejado = totalPlanejado > 0;
|
||||
const percentualDisplay = temPlanejado ? `${Number(percentualVariacao).toFixed(1)}%` : '—';
|
||||
|
||||
return (
|
||||
<div className="space-y-4 sm:space-y-6 mt-4 sm:mt-6 relative pb-6 sm:pb-10">
|
||||
|
|
@ -340,61 +463,45 @@ export const CruzamentoDespesasView = ({ state }) => {
|
|||
</div>
|
||||
|
||||
{/* KPIs - Primeira Linha: APENAS Total Executado alimentado */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 md:gap-6 relative z-10">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-3 sm:gap-4 md:gap-6 relative z-10">
|
||||
<KPICard
|
||||
title="Total Planejado"
|
||||
value={formatCurrency(totalPlanejado)}
|
||||
subtext="Total esperado (despesas V2)"
|
||||
icon={Target}
|
||||
colorClass="text-slate-500"
|
||||
loading={isLoadingTotais}
|
||||
/>
|
||||
<KPICard
|
||||
title="Total Executado"
|
||||
value={formatCurrency(totalExecutado)}
|
||||
subtext="Total realizado (extrato)"
|
||||
icon={CheckCircle2}
|
||||
colorClass="text-emerald-500"
|
||||
colorClass="text-rose-500"
|
||||
highlight
|
||||
loading={isLoadingTotais}
|
||||
/>
|
||||
<KPICard
|
||||
title="Volume Geral"
|
||||
value={formatCurrency(totalPlanejado + totalExecutado)}
|
||||
subtext="Soma de planejado + executado"
|
||||
icon={TrendingUp}
|
||||
colorClass="text-purple-500"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{/* KPIs - Segunda Linha: TODOS ZERADOS */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 md:gap-6 relative z-10">
|
||||
{/* KPIs de Variância */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-3 sm:gap-4 md:gap-6 relative z-10">
|
||||
<KPICard
|
||||
title="Variação"
|
||||
value={formatCurrency(variacao)}
|
||||
subtext={variacao >= 0 ? "Aumento vs planejado" : "Economia vs planejado"}
|
||||
subtext={variacao >= 0 ? "Fora do planejado" : "Dentro do planejado"}
|
||||
icon={BarChart3}
|
||||
trend={variacao > 0 ? 'up' : 'down'}
|
||||
trend={variacao > 0 ? 'down' : 'up'}
|
||||
colorClass={variacao > 0 ? "text-rose-500" : "text-emerald-500"}
|
||||
loading={isLoadingTotais}
|
||||
/>
|
||||
<KPICard
|
||||
title="Taxa de Realização"
|
||||
title="Percentual de diferença"
|
||||
value={`${percentualVariacao}%`}
|
||||
subtext="Percentual do planejado executado"
|
||||
subtext={classificacao || "Percentual do planejado executado"}
|
||||
icon={Activity}
|
||||
colorClass="text-blue-500"
|
||||
/>
|
||||
<KPICard
|
||||
title="Economia"
|
||||
value={variacao < 0 ? formatCurrency(Math.abs(variacao)) : "R$ 0,00"}
|
||||
subtext="Valor abaixo do planejado"
|
||||
icon={TrendingDown}
|
||||
colorClass="text-emerald-500"
|
||||
/>
|
||||
<KPICard
|
||||
title="Excesso"
|
||||
value={variacao > 0 ? formatCurrency(variacao) : "R$ 0,00"}
|
||||
subtext="Valor acima do planejado"
|
||||
icon={AlertCircle}
|
||||
colorClass="text-rose-500"
|
||||
colorClass={corDinamica}
|
||||
loading={isLoadingTotais}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { cn } from '@/lib/utils';
|
|||
import ExcelTable from '../../components/ExcelTable';
|
||||
import { formatDate, formatCurrency } from '../../utils/dateUtils';
|
||||
import { AutoFillInput } from '@/components/shared/AutoFillInput';
|
||||
import { CurrencyInputV2 } from '../../components/CurrencyInputV2';
|
||||
|
||||
const METODOS_PAGAMENTO = [
|
||||
{ value: 'pix', label: 'PIX' },
|
||||
|
|
@ -78,7 +79,7 @@ export const DespesasView = () => {
|
|||
pagoPorMeioDe: '',
|
||||
nomeCliente: '',
|
||||
status: 'PENDENTE',
|
||||
montante: 0,
|
||||
montante: '',
|
||||
categoria: '',
|
||||
descricao: '',
|
||||
metodoPagamento: ''
|
||||
|
|
@ -124,7 +125,7 @@ export const DespesasView = () => {
|
|||
pagoPorMeioDe: '',
|
||||
nomeCliente: '',
|
||||
status: 'PENDENTE',
|
||||
montante: 0,
|
||||
montante: '',
|
||||
categoria: '',
|
||||
descricao: '',
|
||||
metodoPagamento: ''
|
||||
|
|
@ -497,7 +498,7 @@ export const DespesasView = () => {
|
|||
</div>
|
||||
|
||||
{/* Upload de Recibos */}
|
||||
<div>
|
||||
{/* <div>
|
||||
<Label className="text-sm font-bold text-slate-900 dark:text-white mb-3 block">Recibos e Documentos</Label>
|
||||
<div className="border-2 border-dashed border-slate-200 dark:border-slate-800 rounded-xl p-8 text-center bg-slate-50 dark:bg-slate-900/20 group hover:border-rose-400 transition-colors cursor-pointer">
|
||||
<Upload className="w-10 h-10 mx-auto mb-4 text-slate-300 group-hover:text-rose-500 transition-colors" />
|
||||
|
|
@ -505,7 +506,7 @@ export const DespesasView = () => {
|
|||
<p className="text-xs text-slate-500 mb-4">PNG, JPG ou PDF até 10MB</p>
|
||||
<Button size="sm" variant="outline" className="h-8 border-slate-300">Selecionar arquivo</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -609,16 +610,11 @@ export const DespesasView = () => {
|
|||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-500 ml-1">Montante (R$) *</Label>
|
||||
<div className="relative group">
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-black text-sm z-10 pointer-events-none group-focus-within:text-rose-500 transition-colors">R$</span>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.montante}
|
||||
onChange={(e) => setFormData({ ...formData, montante: parseFloat(e.target.value) || 0 })}
|
||||
className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl pl-12 pr-4 font-mono font-bold text-rose-600 focus:ring-rose-500/10 focus:border-rose-500 relative"
|
||||
/>
|
||||
</div>
|
||||
<CurrencyInputV2
|
||||
value={formData.montante}
|
||||
onChange={(val) => setFormData({ ...formData, montante: val || 0 })}
|
||||
className="h-11 bg-slate-50/50 dark:bg-slate-950/50 border-slate-200 dark:border-slate-800 rounded-xl px-4 font-mono font-bold text-rose-600 focus:ring-rose-500/10 focus:border-rose-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
|
@ -706,21 +702,17 @@ export const DespesasView = () => {
|
|||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-500 ml-1">Débito (R$)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
<CurrencyInputV2
|
||||
value={itemFormData.debito}
|
||||
onChange={(e) => setItemFormData({ ...itemFormData, debito: parseFloat(e.target.value) || 0 })}
|
||||
onChange={(val) => setItemFormData({ ...itemFormData, debito: val || 0 })}
|
||||
className="h-11 bg-slate-50/50 border-slate-200 dark:bg-slate-900/50 dark:border-slate-800 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/10 transition-all rounded-xl font-mono text-emerald-600 font-bold text-center"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-500 ml-1">Crédito (R$)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
<CurrencyInputV2
|
||||
value={itemFormData.credito}
|
||||
onChange={(e) => setItemFormData({ ...itemFormData, credito: parseFloat(e.target.value) || 0 })}
|
||||
onChange={(val) => setItemFormData({ ...itemFormData, credito: val || 0 })}
|
||||
className="h-11 bg-slate-50/50 border-slate-200 dark:bg-slate-900/50 dark:border-slate-800 focus:border-rose-500 focus:ring-2 focus:ring-rose-500/10 transition-all rounded-xl font-mono text-rose-600 font-bold text-center"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Building2,
|
||||
Plus,
|
||||
|
|
@ -87,6 +87,7 @@ export const FornecedoresView = () => {
|
|||
const [loadingExtrato, setLoadingExtrato] = useState(false);
|
||||
const [tipoOperacao, setTipoOperacao] = useState('D'); // D para Débito (padrão fornecedores)
|
||||
|
||||
const [serviceType, setServiceType] = useState('servico'); // 'servico' | 'produto'
|
||||
const [availableServices, setAvailableServices] = useState([]);
|
||||
const [availableRules, setAvailableRules] = useState([]); // Regras/Categorias para vínculo
|
||||
|
||||
|
|
@ -99,7 +100,7 @@ export const FornecedoresView = () => {
|
|||
cpf_cnpj: '',
|
||||
tipo_pessoa: 'JURIDICA',
|
||||
servicos: [], // Mantido como array localmente
|
||||
idregra: '', // ID da Regra vinculada
|
||||
regras: [], // Lista de IDs de regras vinculadas
|
||||
obs: '',
|
||||
// Campos opcionais para compatibilidade
|
||||
endereco: '',
|
||||
|
|
@ -131,7 +132,7 @@ export const FornecedoresView = () => {
|
|||
const fetchData = async () => {
|
||||
try {
|
||||
const [services, rulesResponse] = await Promise.all([
|
||||
workspaceReceitasService.fetchServicos(),
|
||||
workspaceReceitasService.fetchServicos(serviceType),
|
||||
conciliacaoService.fetchRules() // Buscando Regras para usar como Regra de Conciliação
|
||||
]);
|
||||
|
||||
|
|
@ -148,7 +149,7 @@ export const FornecedoresView = () => {
|
|||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [serviceType]);
|
||||
|
||||
// Fecth Extrato effect
|
||||
useEffect(() => {
|
||||
|
|
@ -158,17 +159,19 @@ export const FornecedoresView = () => {
|
|||
}
|
||||
|
||||
const fetchExtratoData = async () => {
|
||||
const regraId = selectedFornecedor.idregra || selectedFornecedor.regra_id;
|
||||
|
||||
if (!regraId) {
|
||||
const regraIds = Array.isArray(selectedFornecedor.idregra)
|
||||
? selectedFornecedor.idregra.filter(Boolean)
|
||||
: (selectedFornecedor.idregra ? [selectedFornecedor.idregra] : (selectedFornecedor.regra_id ? [selectedFornecedor.regra_id] : []));
|
||||
|
||||
if (regraIds.length === 0) {
|
||||
setProviderExtrato([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingExtrato(true);
|
||||
try {
|
||||
// Usando a nova rota /regra_aplicada via hook
|
||||
const data = await actions.fetchRegraAplicada(regraId, tipoOperacao);
|
||||
// Passa a lista de IDs de regra; idempresa pode ser passado se disponível no futuro
|
||||
const data = await actions.fetchRegraAplicada(regraIds, tipoOperacao);
|
||||
setProviderExtrato(data || []);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar extrato por regra:', error);
|
||||
|
|
@ -235,17 +238,20 @@ export const FornecedoresView = () => {
|
|||
nome: fornecedor.nome || '',
|
||||
nome_exibicao: fornecedor.nome_exibicao || fornecedor.nome || '',
|
||||
email: fornecedor.email || '',
|
||||
telefone: fornecedor.telefone || '',
|
||||
cpf_cnpj: fornecedor.cpf_cnpj || '',
|
||||
telefone: maskPhone(fornecedor.telefone || ''),
|
||||
cpf_cnpj: maskCPFCNPJ(fornecedor.cpf_cnpj || ''),
|
||||
tipo_pessoa: fornecedor.tipo_pessoa || 'JURIDICA',
|
||||
servicos: parsedServicos,
|
||||
idregra: fornecedor.idregra || fornecedor.regra_id || fornecedor.categoria_id || '', // Mapeia idregra ou regra_id
|
||||
// Normaliza regras como array de IDs (number)
|
||||
regras: Array.isArray(fornecedor.idregra)
|
||||
? fornecedor.idregra.map(Number)
|
||||
: (fornecedor.idregra ? [Number(fornecedor.idregra)] : (fornecedor.regra_id ? [Number(fornecedor.regra_id)] : [])),
|
||||
obs: fornecedor.obs || '',
|
||||
endereco: fornecedor.endereco || '',
|
||||
bairro: fornecedor.bairro || '',
|
||||
cidade: fornecedor.cidade || '',
|
||||
uf: fornecedor.uf || '',
|
||||
cep: fornecedor.cep || '',
|
||||
cep: maskCEP(fornecedor.cep || ''),
|
||||
status: fornecedor.status || 'Ativo',
|
||||
moeda_padrao: fornecedor.moeda_padrao || 'BRL',
|
||||
periodo_vencimento: fornecedor.periodo_vencimento || 'Pagar no recebimento'
|
||||
|
|
@ -261,7 +267,7 @@ export const FornecedoresView = () => {
|
|||
cpf_cnpj: '',
|
||||
tipo_pessoa: 'JURIDICA',
|
||||
servicos: [],
|
||||
idregra: '',
|
||||
regras: [],
|
||||
valor: '',
|
||||
obs: '',
|
||||
endereco: '',
|
||||
|
|
@ -292,8 +298,8 @@ export const FornecedoresView = () => {
|
|||
telefone: cleanTelefone,
|
||||
cpf_cnpj: cleanCPFCNPJ,
|
||||
tipo_pessoa: formData.tipo_pessoa || 'JURIDICA',
|
||||
servico: formData.servicos, // Envia como lista conforme exigido pelo backend
|
||||
idregra: formData.idregra ? Number(formData.idregra) : null, // Novo campo de regra
|
||||
servico: formData.servicos, // Envia como lista
|
||||
idregra: formData.regras.map(Number), // Envia lista de IDs de regras
|
||||
obs: formData.obs || '',
|
||||
// Campos opcionais
|
||||
status: formData.status || 'Ativo',
|
||||
|
|
@ -653,14 +659,23 @@ export const FornecedoresView = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 dark:bg-slate-800/30 p-4 rounded-xl border border-slate-200/60 dark:border-slate-800/50 sm:col-span-2">
|
||||
<Label className="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1.5 block">Regra de Conciliação</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="bg-rose-50/50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-200/50 dark:border-rose-500/20 px-2 py-0.5 text-[11px] font-bold">
|
||||
{getRuleName(selectedFornecedor.regra_id || selectedFornecedor.idregra)}
|
||||
</Badge>
|
||||
<span className="text-[10px] text-slate-500 italic">
|
||||
(ID: {selectedFornecedor.regra_id || selectedFornecedor.idregra || 'N/D'})
|
||||
</span>
|
||||
<Label className="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mb-1.5 block">Regras de Conciliação</Label>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{(() => {
|
||||
const regraIds = Array.isArray(selectedFornecedor.idregra)
|
||||
? selectedFornecedor.idregra
|
||||
: (selectedFornecedor.idregra ? [selectedFornecedor.idregra] : (selectedFornecedor.regra_id ? [selectedFornecedor.regra_id] : []));
|
||||
|
||||
if (regraIds.length === 0) {
|
||||
return <p className="text-xs text-slate-400 italic">Nenhuma regra vinculada</p>;
|
||||
}
|
||||
|
||||
return regraIds.map((rid, idx) => (
|
||||
<Badge key={idx} variant="outline" className="bg-rose-50/50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-200/50 dark:border-rose-500/20 px-2 py-0.5 text-[11px] font-bold">
|
||||
{getRuleName(rid)} <span className="text-[10px] text-slate-400 font-normal ml-1">(#{rid})</span>
|
||||
</Badge>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1150,11 +1165,49 @@ export const FornecedoresView = () => {
|
|||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold text-slate-700 dark:text-slate-300">Serviços Oferecidos</Label>
|
||||
{/* Input simples para atribuir serviços */}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="servico-input"
|
||||
placeholder="Nome do serviço..."
|
||||
className="bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 flex-1"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const val = e.target.value.trim();
|
||||
if (val && !formData.servicos.includes(val)) {
|
||||
setFormData({ ...formData, servicos: [...formData.servicos, val] });
|
||||
toast.success(`Serviço "${val}" atribuído!`);
|
||||
}
|
||||
e.target.value = '';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="bg-rose-600 hover:bg-rose-700 text-white shrink-0"
|
||||
onClick={() => {
|
||||
const input = document.getElementById('servico-input');
|
||||
const val = input?.value?.trim();
|
||||
if (val && !formData.servicos.includes(val)) {
|
||||
setFormData({ ...formData, servicos: [...formData.servicos, val] });
|
||||
toast.success(`Serviço "${val}" atribuído!`);
|
||||
}
|
||||
if (input) input.value = '';
|
||||
}}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* [AutoFillInput comentado para reutilização futura]
|
||||
<AutoFillInput
|
||||
placeholder="Pesquisar e adicionar serviços..."
|
||||
placeholder={`Pesquisar ${serviceType === 'servico' ? 'serviços' : 'produtos'}...`}
|
||||
data={availableServices}
|
||||
filterField="servico"
|
||||
displayField="servico"
|
||||
addNewLabel="ATRIBUIR NOVO SERVIÇO"
|
||||
onSelect={(selectedSvc) => {
|
||||
if (selectedSvc && !formData.servicos.includes(selectedSvc.servico)) {
|
||||
setFormData({
|
||||
|
|
@ -1174,6 +1227,7 @@ export const FornecedoresView = () => {
|
|||
}}
|
||||
className="bg-white dark:bg-slate-800"
|
||||
/>
|
||||
*/}
|
||||
|
||||
{/* Lista de serviços selecionados */}
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
|
|
@ -1202,32 +1256,70 @@ export const FornecedoresView = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Regra Vinculada */}
|
||||
{/* Regras de Conciliação - Multi-seleção */}
|
||||
<div>
|
||||
<Label className="text-xs font-bold text-slate-700 dark:text-slate-300">
|
||||
Regra de Conciliação
|
||||
Regras de Conciliação
|
||||
</Label>
|
||||
<Select
|
||||
value={String(formData.idregra ?? '')}
|
||||
onValueChange={(val) => setFormData({ ...formData, idregra: val === 'no-rule' ? '' : val })}
|
||||
>
|
||||
<SelectTrigger className="mt-1 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
|
||||
<SelectValue placeholder="Selecione uma regra..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="no-rule">Sem regra vinculada</SelectItem>
|
||||
{availableRules.map((rule, index) => (
|
||||
<SelectItem key={String(rule.id ?? rule.idregras_financeiro ?? index)} value={String(rule.id ?? rule.idregras_financeiro ?? '')}>
|
||||
{rule.regra || rule.nome}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(val) => {
|
||||
if (val && val !== 'no-rule') {
|
||||
const numVal = Number(val);
|
||||
if (!formData.regras.includes(numVal)) {
|
||||
setFormData({ ...formData, regras: [...formData.regras, numVal] });
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 flex-1">
|
||||
<SelectValue placeholder="Adicionar regra..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableRules
|
||||
.filter(rule => !formData.regras.includes(Number(rule.id ?? rule.idregras_financeiro)))
|
||||
.map((rule, index) => (
|
||||
<SelectItem key={String(rule.id ?? rule.idregras_financeiro ?? index)} value={String(rule.id ?? rule.idregras_financeiro ?? '')}>
|
||||
{rule.regra || rule.nome}
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* Regras selecionadas como badges */}
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{formData.regras.map((regraId, idx) => {
|
||||
const ruleObj = availableRules.find(r => Number(r.id ?? r.idregras_financeiro) === regraId);
|
||||
return (
|
||||
<Badge
|
||||
key={idx}
|
||||
className="bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-200 dark:border-rose-500/20 px-2 py-1 flex items-center gap-1.5"
|
||||
>
|
||||
{ruleObj ? (ruleObj.regra || ruleObj.nome) : `Regra #${regraId}`}
|
||||
<button
|
||||
onClick={() => setFormData({
|
||||
...formData,
|
||||
regras: formData.regras.filter(r => r !== regraId)
|
||||
})}
|
||||
className="text-rose-400 hover:text-rose-600 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
{formData.regras.length === 0 && (
|
||||
<p className="text-[10px] text-slate-500 italic">Nenhuma regra selecionada</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-500 mt-1">
|
||||
Define a regra automática para transações deste fornecedor.
|
||||
Define as regras automáticas para transações deste fornecedor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Label className="text-xs font-bold text-slate-700 dark:text-slate-300">Observações</Label>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -873,9 +873,9 @@ export const BoletosView = ({ initialFilter = 'TODOS' }) => {
|
|||
|
||||
{/* Dialog de Agendamento */}
|
||||
<Dialog open={isScheduleDialogOpen} onOpenChange={setIsScheduleDialogOpen}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 text-white">
|
||||
<DialogContent className="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white font-bold text-lg">
|
||||
<DialogTitle className="text-slate-900 dark:text-white font-bold text-lg">
|
||||
Agendar Boleto
|
||||
</DialogTitle>
|
||||
<div className="sr-only">Formulário para agendar o vencimento do boleto selecionado</div>
|
||||
|
|
@ -884,22 +884,22 @@ export const BoletosView = ({ initialFilter = 'TODOS' }) => {
|
|||
{selectedBoleto && (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label className="text-slate-300 text-xs font-bold uppercase block mb-2">
|
||||
<Label className="text-slate-500 dark:text-slate-300 text-xs font-bold uppercase block mb-2">
|
||||
Boleto: {selectedBoleto.numero || selectedBoleto.id}
|
||||
</Label>
|
||||
<Label className="text-slate-300 text-xs font-bold uppercase block mb-2">
|
||||
<Label className="text-slate-500 dark:text-slate-300 text-xs font-bold uppercase block mb-2">
|
||||
Pagador: {selectedBoleto.pagador || selectedBoleto.cliente}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-300 text-xs font-bold uppercase">
|
||||
<Label className="text-slate-500 dark:text-slate-300 text-xs font-bold uppercase">
|
||||
Nova Data de Vencimento
|
||||
</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={newDate}
|
||||
onChange={(e) => setNewDate(e.target.value)}
|
||||
className="bg-slate-800 border-slate-700 text-white focus:border-orange-500"
|
||||
className="bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -909,7 +909,7 @@ export const BoletosView = ({ initialFilter = 'TODOS' }) => {
|
|||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setIsScheduleDialogOpen(false)}
|
||||
className="text-slate-400 hover:text-white"
|
||||
className="text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
|
|
@ -1169,28 +1169,28 @@ export const BoletosView = ({ initialFilter = 'TODOS' }) => {
|
|||
function CancelamentoDialog({ open, onOpenChange, onConfirm, loading, reason, setReason, boleto }) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 text-white max-w-md z-[1000]">
|
||||
<DialogContent className="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-white max-w-md z-[1000]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white font-bold text-lg flex items-center gap-2">
|
||||
<DialogTitle className="text-slate-900 dark:text-white font-bold text-lg flex items-center gap-2">
|
||||
<XCircle className="w-5 h-5 text-red-500" />
|
||||
Cancelar Boleto
|
||||
</DialogTitle>
|
||||
<div className="sr-only">Confirmação de cancelamento de boleto</div>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="bg-red-500/5 border border-red-500/10 rounded-lg p-3">
|
||||
<div className="bg-red-50 dark:bg-red-500/5 border border-red-100 dark:border-red-500/10 rounded-lg p-3">
|
||||
<p className="text-xs text-red-500/80">
|
||||
Você está prestes a cancelar o boleto <strong>{boleto?.numero}</strong> de <strong>{boleto?.pagador || boleto?.cliente}</strong> no valor de <strong>{boleto?.valorNominal ? parseFloat(boleto?.valorNominal).toLocaleString('pt-BR', {style: 'currency', currency:'BRL'}) : 'R$ 0,00'}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-300 text-xs font-bold uppercase">
|
||||
<Label className="text-slate-500 dark:text-slate-300 text-xs font-bold uppercase">
|
||||
Motivo do Cancelamento <span className="text-red-400">*</span>
|
||||
</Label>
|
||||
<textarea
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
className="w-full bg-slate-800 border-slate-700 text-white rounded-md p-2 text-sm min-h-[80px]"
|
||||
className="w-full bg-slate-50 dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white rounded-md p-2 text-sm min-h-[80px]"
|
||||
placeholder="Descreva o motivo..."
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1199,7 +1199,7 @@ function CancelamentoDialog({ open, onOpenChange, onConfirm, loading, reason, se
|
|||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="text-slate-400 hover:text-white"
|
||||
className="text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white"
|
||||
disabled={loading}
|
||||
>
|
||||
Voltar
|
||||
|
|
|
|||
|
|
@ -13,13 +13,20 @@ import {
|
|||
Activity,
|
||||
Receipt,
|
||||
ArrowUpCircle,
|
||||
ArrowDownCircle
|
||||
ArrowDownCircle,
|
||||
Percent
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger
|
||||
} from '@/components/ui/tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { extratoService } from '@/services/extratoService';
|
||||
import { boletosService } from '@/services/boletosService';
|
||||
|
|
@ -35,6 +42,7 @@ export const ClientPanel = ({ isOpen, onClose, clients = [] }) => {
|
|||
// Novos estados para dados financeiros
|
||||
const [transactions, setTransactions] = useState([]);
|
||||
const [invoices, setInvoices] = useState([]);
|
||||
const [clientInterest, setClientInterest] = useState([]);
|
||||
const [loadingData, setLoadingData] = useState(false);
|
||||
|
||||
const { getCategoryName, getRuleName } = useStatementRefData();
|
||||
|
|
@ -63,29 +71,30 @@ export const ClientPanel = ({ isOpen, onClose, clients = [] }) => {
|
|||
const loadClientFinancials = async () => {
|
||||
setLoadingData(true);
|
||||
try {
|
||||
const clientName = (selectedClient.nome || '').toLowerCase().trim();
|
||||
const clientDisplay = (selectedClient.nome_exibicao || '').toLowerCase().trim();
|
||||
const clientName = selectedClient.nome || '';
|
||||
const idempresa = selectedClient.idempresa;
|
||||
|
||||
// 1. Busca Extrato (Transações)
|
||||
const allExtrato = await extratoService.fetchExtrato();
|
||||
const filteredExtrato = allExtrato.filter(item => {
|
||||
const beneficiario = (item.beneficiario_pagador || '').toLowerCase();
|
||||
// Verifica correspondência por nome ou nome de exibição
|
||||
return (clientName && beneficiario.includes(clientName)) ||
|
||||
(clientDisplay && beneficiario.includes(clientDisplay));
|
||||
});
|
||||
setTransactions(filteredExtrato);
|
||||
// 1. Busca Transações via nova rota /beneficiario_aplicado
|
||||
const transactionsData = await extratoService.fetchBeneficiarioAplicado(clientName);
|
||||
setTransactions(transactionsData);
|
||||
|
||||
// 2. Busca Boletos (Faturas)
|
||||
const boletosData = await boletosService.fetchBoletos();
|
||||
// 2. Busca Boletos (Faturas) - Mantido fetchBoletosStatus por enquanto ou filtragem local se necessário
|
||||
// O usuário não pediu para mudar a rota de faturas, mas pediu para mudar a de transações
|
||||
const boletosData = await boletosService.fetchBoletosStatus();
|
||||
const allBoletos = boletosData.cobrancas || [];
|
||||
const filteredBoletos = allBoletos.filter(item => {
|
||||
const pagador = (item.pagador || item.cliente || '').toLowerCase();
|
||||
return (clientName && pagador.includes(clientName)) ||
|
||||
(clientDisplay && pagador.includes(clientDisplay));
|
||||
const nameLower = clientName.toLowerCase();
|
||||
return pagador.includes(nameLower);
|
||||
});
|
||||
setInvoices(filteredBoletos);
|
||||
|
||||
// 3. Busca Juros Detalhe via nova rota /financeiro/cliente/boletos
|
||||
if (idempresa) {
|
||||
const interestData = await boletosService.fetchJurosCliente(idempresa);
|
||||
setClientInterest(interestData);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar dados financeiros do cliente", error);
|
||||
} finally {
|
||||
|
|
@ -234,91 +243,152 @@ export const ClientPanel = ({ isOpen, onClose, clients = [] }) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Faturas Recentes */}
|
||||
<Card className="border border-slate-100 shadow-sm bg-white overflow-hidden rounded-3xl">
|
||||
<div className="p-6 border-b border-slate-50 flex justify-between items-center bg-slate-50/50">
|
||||
<h4 className="font-bold text-slate-800 text-[10px] uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-emerald-100 flex items-center justify-center">
|
||||
<CreditCard className="w-4 h-4 text-emerald-600" />
|
||||
</div>
|
||||
Faturas & Boletos
|
||||
</h4>
|
||||
<Button size="sm" variant="ghost" className="h-8 text-[10px] font-bold uppercase text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 tracking-widest">
|
||||
{loadingData ? 'Carregando...' : `${invoices.length} faturas`}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[300px] overflow-y-auto p-2">
|
||||
{invoices.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-400 text-xs font-medium">
|
||||
Nenhuma fatura encontrada.
|
||||
</div>
|
||||
) : (
|
||||
invoices.slice(0, 10).map((invoice, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-4 hover:bg-slate-50 rounded-2xl transition-colors group">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-slate-100 flex items-center justify-center group-hover:bg-white transition-colors">
|
||||
<FileText className="w-5 h-5 text-slate-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-slate-900 underline decoration-slate-200 group-hover:decoration-emerald-500/30 transition-all">
|
||||
{invoice.descricao || `Fatura #${invoice.numero || i}`}
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-500 font-medium">
|
||||
Vencimento: {formatDate(invoice.vencimento || invoice.dataVencimento)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-slate-900">{formatCurrency(invoice.valorNominal || invoice.valor)}</p>
|
||||
<Badge variant="outline" className={cn(
|
||||
"text-[8px] font-bold px-2 h-4 border-none",
|
||||
(invoice.status === 'PAGO' || invoice.status === 'RECEBIDO') ? "bg-emerald-100 text-emerald-700" :
|
||||
(invoice.status === 'ATRASADO' || invoice.status === 'VENCIDO') ? "bg-red-100 text-red-700" :
|
||||
"bg-slate-100 text-slate-600"
|
||||
)}>
|
||||
{invoice.status || 'PENDENTE'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<Tabs defaultValue="faturas" className="w-full">
|
||||
<TabsList className="w-full justify-start bg-slate-50/50 border border-slate-100 p-1 h-12 rounded-2xl mb-6">
|
||||
<TabsTrigger value="faturas" className="flex-1 rounded-xl data-[state=active]:bg-white data-[state=active]:text-emerald-600 data-[state=active]:shadow-sm font-bold text-[10px] uppercase tracking-wider">
|
||||
<CreditCard className="w-3.5 h-3.5 mr-2" /> Faturas
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="transacoes" className="flex-1 rounded-xl data-[state=active]:bg-white data-[state=active]:text-blue-600 data-[state=active]:shadow-sm font-bold text-[10px] uppercase tracking-wider">
|
||||
<Receipt className="w-3.5 h-3.5 mr-2" /> Transações
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="juros" className="flex-1 rounded-xl data-[state=active]:bg-white data-[state=active]:text-amber-600 data-[state=active]:shadow-sm font-bold text-[10px] uppercase tracking-wider">
|
||||
<Percent className="w-3.5 h-3.5 mr-2" /> Juros
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Extrato / Transações */}
|
||||
<Card className="border border-slate-100 shadow-sm bg-white overflow-hidden rounded-3xl">
|
||||
<div className="p-6 border-b border-slate-50 flex justify-between items-center bg-slate-50/50">
|
||||
<h4 className="font-bold text-slate-800 text-[10px] uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Receipt className="w-4 h-4 text-blue-600" />
|
||||
<TabsContent value="faturas">
|
||||
<Card className="border border-slate-100 shadow-sm bg-white overflow-hidden rounded-3xl">
|
||||
<div className="p-6 border-b border-slate-50 flex justify-between items-center bg-slate-50/50">
|
||||
<h4 className="font-bold text-slate-800 text-[10px] uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-emerald-100 flex items-center justify-center">
|
||||
<CreditCard className="w-4 h-4 text-emerald-600" />
|
||||
</div>
|
||||
Faturas & Boletos
|
||||
</h4>
|
||||
<Button size="sm" variant="ghost" className="h-8 text-[10px] font-bold uppercase text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 tracking-widest leading-none">
|
||||
{loadingData ? '...' : `${invoices.length} faturas`}
|
||||
</Button>
|
||||
</div>
|
||||
Extrato & Transações
|
||||
</h4>
|
||||
<Button size="sm" variant="ghost" className="h-8 text-[10px] font-bold uppercase text-blue-600 hover:text-blue-700 hover:bg-blue-50 tracking-widest">
|
||||
{loadingData ? 'Carregando...' : `${transactions.length} registros`}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[300px] overflow-y-auto p-2">
|
||||
{transactions.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-400 text-xs font-medium">
|
||||
Nenhuma transação encontrada no extrato.
|
||||
<div className="max-h-[350px] overflow-y-auto p-2">
|
||||
{invoices.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-400 text-xs font-medium">
|
||||
Nenhuma fatura encontrada.
|
||||
</div>
|
||||
) : (
|
||||
invoices.slice(0, 10).map((invoice, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-4 hover:bg-slate-50 rounded-2xl transition-colors group">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-slate-100 flex items-center justify-center group-hover:bg-white transition-colors">
|
||||
<FileText className="w-5 h-5 text-slate-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-slate-900 underline decoration-slate-200 group-hover:decoration-emerald-500/30 transition-all">
|
||||
{invoice.descricao || `Fatura #${invoice.numero || i}`}
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-500 font-medium">
|
||||
Vencimento: {formatDate(invoice.vencimento || invoice.dataVencimento)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-slate-900">{formatCurrency(invoice.valorNominal || invoice.valor)}</p>
|
||||
<Badge variant="outline" className={cn(
|
||||
"text-[8px] font-bold px-2 h-4 border-none",
|
||||
(invoice.status === 'PAGO' || invoice.status === 'RECEBIDO') ? "bg-emerald-100 text-emerald-700" :
|
||||
(invoice.status === 'ATRASADO' || invoice.status === 'VENCIDO') ? "bg-red-100 text-red-700" :
|
||||
"bg-slate-100 text-slate-600"
|
||||
)}>
|
||||
{invoice.status || 'PENDENTE'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
transactions.slice(0, 10).map((transaction, i) => (
|
||||
<StatementRow
|
||||
key={transaction.id || i}
|
||||
transaction={transaction}
|
||||
categoryName={getCategoryName(transaction.categoria)}
|
||||
ruleName={getRuleName(transaction.regra)}
|
||||
showCategory={true}
|
||||
showStatus={false}
|
||||
showBeneficiary={false}
|
||||
className="rounded-2xl border border-transparent hover:border-slate-100 hover:bg-slate-50"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="transacoes">
|
||||
<Card className="border border-slate-100 shadow-sm bg-white overflow-hidden rounded-3xl">
|
||||
<div className="p-6 border-b border-slate-50 flex justify-between items-center bg-slate-50/50">
|
||||
<h4 className="font-bold text-slate-800 text-[10px] uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<Receipt className="w-4 h-4 text-blue-600" />
|
||||
</div>
|
||||
Extrato & Transações
|
||||
</h4>
|
||||
<Button size="sm" variant="ghost" className="h-8 text-[10px] font-bold uppercase text-blue-600 hover:text-blue-700 hover:bg-blue-50 tracking-widest leading-none">
|
||||
{loadingData ? '...' : `${transactions.length} registros`}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[350px] overflow-y-auto p-2">
|
||||
{transactions.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-400 text-xs font-medium">
|
||||
Nenhuma transação encontrada no extrato.
|
||||
</div>
|
||||
) : (
|
||||
transactions.slice(0, 10).map((transaction, i) => (
|
||||
<StatementRow
|
||||
key={transaction.id || i}
|
||||
transaction={transaction}
|
||||
categoryName={getCategoryName(transaction.categoria)}
|
||||
ruleName={getRuleName(transaction.regra)}
|
||||
showCategory={true}
|
||||
showStatus={false}
|
||||
showBeneficiary={false}
|
||||
className="rounded-2xl border border-transparent hover:border-slate-100 hover:bg-slate-50"
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="juros">
|
||||
<Card className="border border-slate-100 shadow-sm bg-white overflow-hidden rounded-3xl">
|
||||
<div className="p-6 border-b border-slate-50 flex justify-between items-center bg-slate-50/50">
|
||||
<h4 className="font-bold text-slate-800 text-[10px] uppercase tracking-[0.2em] flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center">
|
||||
<Percent className="w-4 h-4 text-amber-600" />
|
||||
</div>
|
||||
Detalhamento de Juros
|
||||
</h4>
|
||||
<Button size="sm" variant="ghost" className="h-8 text-[10px] font-bold uppercase text-amber-600 hover:text-amber-700 hover:bg-amber-50 tracking-widest leading-none">
|
||||
{loadingData ? '...' : `${clientInterest.length} registros`}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-h-[350px] overflow-y-auto p-2">
|
||||
{clientInterest.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-400 text-xs font-medium">
|
||||
Nenhum detalhe de juros encontrado.
|
||||
</div>
|
||||
) : (
|
||||
clientInterest.map((item, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-4 hover:bg-slate-50 rounded-2xl transition-colors group">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center group-hover:bg-white transition-colors">
|
||||
<Percent className="w-5 h-5 text-amber-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-slate-900">
|
||||
{item.descricao || `Juros #${item.numero || i}`}
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-500 font-medium">
|
||||
Data: {formatDate(item.data || item.data_vencimento)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-amber-600">+{formatCurrency(item.valor_juros || item.juros || 0)}</p>
|
||||
<p className="text-[9px] text-slate-400 font-bold uppercase tracking-tighter">Valor Base: {formatCurrency(item.valor || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex gap-4 pt-4">
|
||||
<Button className="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-900 border-none font-bold text-[10px] uppercase tracking-widest h-12 shadow-sm">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { extratoService } from '@/services/extratoService';
|
||||
import { boletosService } from '@/services/boletosService';
|
||||
import {
|
||||
Users,
|
||||
User,
|
||||
|
|
@ -28,7 +29,8 @@ import {
|
|||
ArrowUpCircle,
|
||||
ArrowDownCircle,
|
||||
AlertCircle,
|
||||
Wrench
|
||||
Wrench,
|
||||
Percent
|
||||
} from 'lucide-react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
|
@ -114,6 +116,9 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
|||
const [clientExtrato, setClientExtrato] = useState([]);
|
||||
const [loadingExtrato, setLoadingExtrato] = useState(false);
|
||||
|
||||
const [clientInterest, setClientInterest] = useState([]);
|
||||
const [loadingInterest, setLoadingInterest] = useState(false);
|
||||
|
||||
const [clientFaturaUrl, setClientFaturaUrl] = useState(null);
|
||||
const [loadingFatura, setLoadingFatura] = useState(false);
|
||||
const [clientServicosAtribuidos, setClientServicosAtribuidos] = useState([]);
|
||||
|
|
@ -244,53 +249,48 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
|||
|
||||
const fetchExtratoData = async () => {
|
||||
setLoadingExtrato(true);
|
||||
const clientName = (selectedClient.nome || '').toLowerCase().trim();
|
||||
const clientDisplay = (selectedClient.nome_exibicao || '').toLowerCase().trim();
|
||||
const clientName = selectedClient.nome || '';
|
||||
|
||||
console.group('🔍 Buscando Extrato para Cliente');
|
||||
console.log('Cliente Selecionado:', selectedClient);
|
||||
console.log('Nome para filtro:', clientName);
|
||||
console.log('Nome Exibição para filtro:', clientDisplay);
|
||||
|
||||
try {
|
||||
const allExtrato = await extratoService.fetchExtrato();
|
||||
console.log('Total de transações recebidas:', allExtrato?.length);
|
||||
|
||||
if (Array.isArray(allExtrato)) {
|
||||
const filteredExtrato = allExtrato.filter(item => {
|
||||
const beneficiario = (item.beneficiario_pagador || '').toLowerCase();
|
||||
const descricao = (item.descricao || '').toLowerCase();
|
||||
|
||||
// Verifica se o beneficiário ou descrição contém o nome do cliente
|
||||
const matchName = clientName && (beneficiario.includes(clientName) || descricao.includes(clientName));
|
||||
const matchDisplay = clientDisplay && (beneficiario.includes(clientDisplay) || descricao.includes(clientDisplay));
|
||||
|
||||
// Log para debugging de correspondência (apenas para os primeiros 5 ou se der match)
|
||||
// if (matchName || matchDisplay) {
|
||||
// console.log('✅ Match encontrado:', { beneficiario, descricao, matchName, matchDisplay });
|
||||
// }
|
||||
|
||||
return matchName || matchDisplay;
|
||||
});
|
||||
|
||||
console.log('Transações filtradas:', filteredExtrato.length);
|
||||
console.table(filteredExtrato.slice(0, 5)); // Mostra as primeiras 5
|
||||
setClientExtrato(filteredExtrato);
|
||||
} else {
|
||||
console.warn('⚠️ Resposta do extrato não é um array:', allExtrato);
|
||||
setClientExtrato([]);
|
||||
}
|
||||
|
||||
// Agora usa a rota otimizada /beneficiario_aplicado
|
||||
const data = await extratoService.fetchBeneficiarioAplicado(clientName);
|
||||
setClientExtrato(data);
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao buscar extrato:', error);
|
||||
setClientExtrato([]);
|
||||
} finally {
|
||||
setLoadingExtrato(false);
|
||||
console.groupEnd();
|
||||
}
|
||||
};
|
||||
|
||||
fetchExtratoData();
|
||||
}, [clientTab, selectedClient?.idempresa ?? selectedClient?.id, selectedClient?.nome]);
|
||||
|
||||
// Carrega detalhes de juros quando abrir a aba Juros
|
||||
useEffect(() => {
|
||||
if (clientTab !== 'juros' || !selectedClient) {
|
||||
setClientInterest([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchInterestData = async () => {
|
||||
setLoadingInterest(true);
|
||||
const idempresa = selectedClient.idempresa || selectedClient.id;
|
||||
|
||||
try {
|
||||
if (idempresa) {
|
||||
const data = await boletosService.fetchJurosCliente(idempresa);
|
||||
setClientInterest(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erro ao buscar juros:', error);
|
||||
setClientInterest([]);
|
||||
} finally {
|
||||
setLoadingInterest(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchInterestData();
|
||||
}, [clientTab, selectedClient?.idempresa ?? selectedClient?.id]);
|
||||
|
||||
// Carrega caixinhas ao abrir o Dialog de cliente (mesma rota da conciliação: GET /caixinhas/apresentar)
|
||||
|
|
@ -952,6 +952,9 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
|||
<TabsTrigger value="faturas" className="text-xs sm:text-sm font-medium data-[state=active]:border-b-2 data-[state=active]:border-blue-600 data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 rounded-none pb-3 text-slate-600 dark:text-slate-400 whitespace-nowrap">
|
||||
Faturas
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="juros" className="text-xs sm:text-sm font-medium data-[state=active]:border-b-2 data-[state=active]:border-amber-600 data-[state=active]:text-amber-600 dark:data-[state=active]:text-amber-400 rounded-none pb-3 text-slate-600 dark:text-slate-400 whitespace-nowrap">
|
||||
Juros
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="servicos-produtos" className="text-xs sm:text-sm font-medium data-[state=active]:border-b-2 data-[state=active]:border-blue-600 data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400 rounded-none pb-3 text-slate-600 dark:text-slate-400 whitespace-nowrap">
|
||||
Serviços/Produtos
|
||||
</TabsTrigger>
|
||||
|
|
@ -1175,15 +1178,14 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
|||
{loadingExtrato ? (
|
||||
<div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400">
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
<span className="text-sm">Analisando transações...</span>
|
||||
<span className="text-sm">Buscando transações via /beneficiario_aplicado...</span>
|
||||
</div>
|
||||
) : clientExtrato.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
||||
<p className="text-sm font-medium">Nenhuma transação encontrada</p>
|
||||
<p className="text-xs mt-2 max-w-[300px] mx-auto opacity-70">
|
||||
Verificamos o extrato buscando por "{selectedClient.nome}" ou "{selectedClient.nome_exibicao || ''}".
|
||||
<br/>Verifique os logs (F12) para detalhes.
|
||||
Consultamos o beneficiário "{selectedClient.nome}" e não retornou registros.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -1393,6 +1395,79 @@ export const ClientsView = ({ clients: initialClients = [] }) => {
|
|||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="juros" className="flex-1 overflow-auto p-4 sm:p-6 m-0 bg-white dark:bg-slate-900/50">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-base sm:text-lg font-bold text-slate-900 dark:text-white">Detalhamento de Juros</h4>
|
||||
<Badge variant="outline" className="text-xs bg-amber-500/10 text-amber-600 border-amber-500/20">
|
||||
{clientInterest.length} registros
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{loadingInterest ? (
|
||||
<div className="flex items-center justify-center py-12 gap-2 text-slate-500 dark:text-slate-400">
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
<span className="text-sm">Buscando juros via /financeiro/cliente/boletos...</span>
|
||||
</div>
|
||||
) : clientInterest.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<Percent className="w-12 h-12 mx-auto mb-4 text-slate-400" />
|
||||
<p className="text-sm font-medium">Nenhum detalhe de juros encontrado</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-[500px]">
|
||||
<ExcelTable
|
||||
data={clientInterest}
|
||||
columns={[
|
||||
{
|
||||
field: 'data',
|
||||
header: 'Data',
|
||||
width: '120px',
|
||||
render: (row) => (
|
||||
<span className="text-slate-600 dark:text-slate-400">
|
||||
{formatDate(row.data || row.data_vencimento)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'descricao',
|
||||
header: 'Descrição',
|
||||
width: '300px',
|
||||
render: (row) => (
|
||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||
{row.descricao || `Juros #${row.numero || ''}`}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'valor_juros',
|
||||
header: 'Valor Juros',
|
||||
width: '150px',
|
||||
render: (row) => (
|
||||
<span className="font-bold text-amber-600">
|
||||
+{formatCurrency(row.valor_juros || row.juros || 0)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'valor',
|
||||
header: 'Valor Base',
|
||||
width: '150px',
|
||||
render: (row) => (
|
||||
<span className="text-slate-500 text-sm">
|
||||
{formatCurrency(row.valor || 0)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
]}
|
||||
rowKey={(row, i) => i}
|
||||
pageSize={15}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export const ContasReceberView = ({ initialView }) => {
|
|||
<div className="flex items-center justify-between gap-4 mb-4 sm:mb-6">
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-slate-900 dark:text-white tracking-tight flex items-center gap-2 sm:gap-3">
|
||||
Recebíveis
|
||||
<Badge className="bg-emerald-500/10 text-emerald-500 border-emerald-500/20 text-[10px] hidden sm:inline-flex">PREMIUM v2</Badge>
|
||||
{/* <Badge className="bg-emerald-500/10 text-emerald-500 border-emerald-500/20 text-[10px] hidden sm:inline-flex">PREMIUM v2</Badge> */}
|
||||
</h1>
|
||||
|
||||
{/* <Button
|
||||
|
|
|
|||
|
|
@ -199,26 +199,57 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
|||
fetchTotais();
|
||||
}, [filtroMes, filtroAno, filtroTipo]);
|
||||
|
||||
const [chartDataBackend, setChartDataBackend] = React.useState([]);
|
||||
const [isLoadingChart, setIsLoadingChart] = React.useState(false);
|
||||
|
||||
// Efeito para buscar dados do gráfico do backend
|
||||
React.useEffect(() => {
|
||||
const fetchChartData = async () => {
|
||||
setIsLoadingChart(true);
|
||||
try {
|
||||
const params = {
|
||||
ano: filtroAno
|
||||
};
|
||||
if (filtroTipo === 'mes') {
|
||||
params.mes = filtroMes;
|
||||
}
|
||||
// Nota: o sistema atual não parece ter seletor de 'dia' global aqui,
|
||||
// mas a rota suporta. Se houver dia no futuro, adicione aqui.
|
||||
|
||||
const result = await extratoService.fetchPlanejadoGrafico(params);
|
||||
const data = result?.grafico ?? result ?? [];
|
||||
setChartDataBackend(Array.isArray(data) ? data : []);
|
||||
} catch (err) {
|
||||
console.error('[CruzamentoView] Erro ao buscar dados do gráfico:', err);
|
||||
setChartDataBackend([]);
|
||||
} finally {
|
||||
setIsLoadingChart(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChartData();
|
||||
}, [filtroMes, filtroAno, filtroTipo]);
|
||||
|
||||
// KPIs recalculados com dados filtrados (ou vindos do backend se for mensal)
|
||||
const kpisFiltrados = React.useMemo(() => {
|
||||
if (filtroTipo === 'mes' && totaisPlanejados) {
|
||||
return {
|
||||
totalRecebido: totaisPlanejados.total_realizado || 0,
|
||||
totalPendente: totaisPlanejados.total_boletos_a_receber || 0,
|
||||
totalGeral: totaisPlanejados.total_planejado_geral || 0
|
||||
total_realizado: totaisPlanejados.total_realizado || 0,
|
||||
total_boletos_a_receber: totaisPlanejados.total_boletos_a_receber || 0,
|
||||
total_planejado_geral: totaisPlanejados.total_planejado_geral || 0
|
||||
};
|
||||
}
|
||||
|
||||
const totalRecebido = dadosFiltrados
|
||||
const total_realizado = dadosFiltrados
|
||||
.filter(t => t?.status === 'Recebido' || t?.status === 'Liquidado')
|
||||
.reduce((acc, t) => acc + (t?.valor || 0), 0);
|
||||
|
||||
const totalPendente = planejadasFiltradas.reduce((acc, t) => acc + (Number(t?.total || 0)), 0);
|
||||
const total_boletos_a_receber = planejadasFiltradas.reduce((acc, t) => acc + (Number(t?.total || 0)), 0);
|
||||
|
||||
return {
|
||||
totalRecebido,
|
||||
totalPendente,
|
||||
totalGeral: totalRecebido + totalPendente
|
||||
total_realizado,
|
||||
total_boletos_a_receber,
|
||||
total_planejado_geral: total_realizado + total_boletos_a_receber
|
||||
};
|
||||
}, [dadosFiltrados, planejadasFiltradas, filtroTipo, totaisPlanejados]);
|
||||
|
||||
|
|
@ -290,6 +321,30 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
|||
|
||||
// Dados comparativos: planejado vs executado
|
||||
const comparativoData = useMemo(() => {
|
||||
if (chartDataBackend.length > 0) {
|
||||
return chartDataBackend.map(item => {
|
||||
const variacao = (item.executado || 0) - (item.planejado || 0);
|
||||
const percentual = (item.planejado || 0) > 0 ? (((item.executado || 0) / item.planejado) * 100).toFixed(1) : 0;
|
||||
|
||||
// Formata o nome do período para exibição
|
||||
let name = String(item.periodo);
|
||||
if (filtroTipo === 'ano') {
|
||||
const mesesLabel = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
||||
const mesIdx = parseInt(item.periodo) - 1;
|
||||
if (mesIdx >= 0 && mesIdx < 12) name = mesesLabel[mesIdx];
|
||||
} else if (filtroTipo === 'mes') {
|
||||
name = `${String(item.periodo).padStart(2, '0')}/${String(filtroMes).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
name,
|
||||
variacao,
|
||||
percentual
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const mesesLabel = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
||||
const map = {};
|
||||
|
||||
|
|
@ -338,9 +393,9 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
|||
.map(([name, values]) => {
|
||||
const variacao = values.executado - values.planejado;
|
||||
const percentual = values.planejado > 0 ? ((values.executado / values.planejado) * 100).toFixed(1) : 0;
|
||||
return { ...values, variacao, percentual };
|
||||
return { ...values, name, variacao, percentual };
|
||||
});
|
||||
}, [dadosFiltrados, planejadasFiltradas, filtroTipo]);
|
||||
}, [dadosFiltrados, planejadasFiltradas, filtroTipo, chartDataBackend, filtroMes]);
|
||||
|
||||
// Análise de Variância baseada exclusivamente na rota de soma por categoria
|
||||
const variacaoCategoria = useMemo(() => {
|
||||
|
|
@ -367,30 +422,36 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
|||
// KPIs de variância: Usando os dados reais
|
||||
const kpisVariacao = useMemo(() => {
|
||||
if (filtroTipo === 'mes' && totaisPlanejados) {
|
||||
const totalExecutado = totaisPlanejados.total_realizado || 0;
|
||||
const totalPlanejado = totaisPlanejados.total_planejado || 0;
|
||||
const variacaoTotal = totaisPlanejados.diferenca || 0;
|
||||
const percentualVariacao = totalPlanejado > 0 ? ((totalExecutado / totalPlanejado) * 100).toFixed(1) : '0';
|
||||
|
||||
return {
|
||||
totalExecutado,
|
||||
totalPlanejado,
|
||||
variacaoTotal,
|
||||
percentualVariacao
|
||||
total_realizado: totaisPlanejados.total_realizado || 0,
|
||||
total_planejado: totaisPlanejados.total_planejado || 0,
|
||||
total_boletos_a_receber: totaisPlanejados.total_boletos_a_receber || 0,
|
||||
diferenca: totaisPlanejados.diferenca || 0,
|
||||
percentual_variacao: (totaisPlanejados.total_planejado || 0) > 0
|
||||
? ((totaisPlanejados.total_realizado / totaisPlanejados.total_planejado) * 100).toFixed(1)
|
||||
: '0'
|
||||
};
|
||||
}
|
||||
|
||||
const totalExecutado = kpisFiltrados.totalRecebido;
|
||||
const totalPlanejado = kpisFiltrados.totalPendente;
|
||||
const variacaoTotal = totalExecutado - totalPlanejado;
|
||||
const percentualVariacao = totalPlanejado > 0 ? ((totalExecutado / totalPlanejado) * 100).toFixed(1) : '0';
|
||||
const total_realizado = kpisFiltrados.total_realizado;
|
||||
const total_planejado = planejadasFiltradas
|
||||
.filter(p => !p.parcelado) // Exemplo de lógica local se necessário
|
||||
.reduce((acc, p) => acc + (Number(p.total || 0)), 0);
|
||||
|
||||
const total_boletos_a_receber = kpisFiltrados.total_boletos_a_receber;
|
||||
const diferenca = total_realizado - (total_planejado + total_boletos_a_receber);
|
||||
const percentual_variacao = (total_planejado + total_boletos_a_receber) > 0
|
||||
? ((total_realizado / (total_planejado + total_boletos_a_receber)) * 100).toFixed(1)
|
||||
: '0';
|
||||
|
||||
return {
|
||||
totalExecutado,
|
||||
totalPlanejado,
|
||||
variacaoTotal,
|
||||
percentualVariacao
|
||||
total_realizado,
|
||||
total_planejado,
|
||||
total_boletos_a_receber,
|
||||
diferenca,
|
||||
percentual_variacao
|
||||
};
|
||||
}, [kpisFiltrados, filtroTipo, totaisPlanejados]);
|
||||
}, [kpisFiltrados, filtroTipo, totaisPlanejados, planejadasFiltradas]);
|
||||
|
||||
const COLORS = ['#10b981', '#3b82f6', '#f59e0b', '#8b5cf6', '#ec4899', '#f43f5e'];
|
||||
|
||||
|
|
@ -478,27 +539,27 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPIs: TODOS ZERADOS exceto Total Executado */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 md:gap-6 relative z-10">
|
||||
{/* KPIs Principais */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-3 sm:gap-4 md:gap-6 relative z-10">
|
||||
<KPICard
|
||||
title="Liquidado"
|
||||
value={formatCurrency(kpisFiltrados.totalRecebido)}
|
||||
value={formatCurrency(kpisFiltrados.total_realizado)}
|
||||
subtext="Total recebido efetivamente"
|
||||
icon={CheckCircle2}
|
||||
colorClass="text-emerald-500"
|
||||
loading={isLoadingTotais}
|
||||
/>
|
||||
<KPICard
|
||||
{/* <KPICard
|
||||
title="Expectativa"
|
||||
value={formatCurrency(kpisFiltrados.totalPendente)}
|
||||
value={formatCurrency(kpisFiltrados.total_planejado)}
|
||||
subtext="Total planejado + boletos á receber p/ o período"
|
||||
icon={Clock}
|
||||
colorClass="text-blue-500"
|
||||
loading={isLoadingTotais}
|
||||
/>
|
||||
/> */}
|
||||
<KPICard
|
||||
title="Volume Geral"
|
||||
value={formatCurrency(kpisFiltrados.totalGeral)}
|
||||
value={formatCurrency(kpisFiltrados.total_planejado_geral)}
|
||||
subtext="Soma de realizado + esperado"
|
||||
icon={TrendingUp}
|
||||
colorClass="text-purple-500"
|
||||
|
|
@ -506,20 +567,20 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* KPIs de Variância: APENAS Total Executado alimentado */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 md:gap-6 relative z-10">
|
||||
{/* KPIs de Variância */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 md:gap-6 relative z-10">
|
||||
<KPICard
|
||||
title="Expectativa (Plan)"
|
||||
value={formatCurrency(kpisVariacao.totalPlanejado)}
|
||||
value={formatCurrency(kpisVariacao.total_planejado)}
|
||||
subtext="Total de entradas planejadas"
|
||||
icon={Target}
|
||||
colorClass="text-blue-500"
|
||||
loading={isLoadingTotais}
|
||||
/>
|
||||
<KPICard
|
||||
title="Realizado (Exec)"
|
||||
value={formatCurrency(kpisVariacao.totalExecutado)}
|
||||
subtext="Realizado efetivamente (extrato)"
|
||||
title="Boletos á receber"
|
||||
value={formatCurrency(kpisVariacao.total_boletos_a_receber)}
|
||||
subtext="Boletos á receber no perido"
|
||||
icon={CheckCircle2}
|
||||
colorClass="text-emerald-500"
|
||||
highlight
|
||||
|
|
@ -527,21 +588,21 @@ export const CruzamentoView = ({ data = [], kpis = {}, caixas = [], entradasPlan
|
|||
/>
|
||||
<KPICard
|
||||
title="Variação"
|
||||
value={formatCurrency(kpisVariacao.variacaoTotal)}
|
||||
subtext={kpisVariacao.variacaoTotal >= 0 ? "Acima do planejado" : "Abaixo do planejado"}
|
||||
value={formatCurrency(kpisVariacao.diferenca)}
|
||||
subtext={kpisVariacao.diferenca >= 0 ? "Acima do planejado" : "Abaixo do planejado"}
|
||||
icon={BarChart3}
|
||||
trend={kpisVariacao.variacaoTotal >= 0 ? 'up' : 'down'}
|
||||
colorClass={kpisVariacao.variacaoTotal >= 0 ? "text-emerald-500" : "text-rose-500"}
|
||||
trend={kpisVariacao.diferenca >= 0 ? 'up' : 'down'}
|
||||
colorClass={kpisVariacao.diferenca >= 0 ? "text-emerald-500" : "text-rose-500"}
|
||||
loading={isLoadingTotais}
|
||||
/>
|
||||
<KPICard
|
||||
{/* <KPICard
|
||||
title="Taxa de Entrega"
|
||||
value={`${kpisVariacao.percentualVariacao}%`}
|
||||
subtext="Realizado vs Planejado"
|
||||
icon={Activity}
|
||||
colorClass="text-purple-500"
|
||||
loading={isLoadingTotais}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
|
||||
{/* Gráfico Comparativo Planejado vs Executado */}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { useToast } from '../../hooks/useToast';
|
|||
import { cn } from '@/lib/utils';
|
||||
import ExcelTable from '../../components/ExcelTable';
|
||||
import { formatDate, formatCurrency } from '../../utils/dateUtils';
|
||||
import { CurrencyInputV2 } from '../../components/CurrencyInputV2';
|
||||
|
||||
export const EntradasPlanejadasView = () => {
|
||||
const { state, actions } = useContasReceber();
|
||||
|
|
@ -77,6 +78,7 @@ export const EntradasPlanejadasView = () => {
|
|||
|
||||
// Estados para o campo inteligente de serviços
|
||||
const [servicos, setServicos] = useState([]);
|
||||
const [catalogType, setCatalogType] = useState('servico'); // 'servico' | 'produto'
|
||||
const [searchTermServico, setSearchTermServico] = useState('');
|
||||
const [showServicoSuggestions, setShowServicoSuggestions] = useState(false);
|
||||
const [activeItemIndex, setActiveItemIndex] = useState(null);
|
||||
|
|
@ -123,7 +125,7 @@ export const EntradasPlanejadasView = () => {
|
|||
// Carrega a lista de serviços para o campo inteligente
|
||||
const fetchServicos = async () => {
|
||||
try {
|
||||
const data = await actions?.loadServicos?.();
|
||||
const data = await actions?.loadServicos?.(catalogType);
|
||||
setServicos(Array.isArray(data) ? data : []);
|
||||
} catch (err) {
|
||||
console.error('[EntradasPlanejadasView] Erro ao carregar serviços:', err);
|
||||
|
|
@ -131,7 +133,7 @@ export const EntradasPlanejadasView = () => {
|
|||
}
|
||||
};
|
||||
fetchServicos();
|
||||
}, [actions.loadEntradasPlanejadas, actions.loadServicos]);
|
||||
}, [actions.loadEntradasPlanejadas, actions.loadServicos, catalogType]);
|
||||
|
||||
const handleOpenDialog = async (entrada = null) => {
|
||||
if (entrada) {
|
||||
|
|
@ -892,10 +894,38 @@ export const EntradasPlanejadasView = () => {
|
|||
|
||||
{/* Seletor de Serviços (Movido para fora da lista) */}
|
||||
<div className="relative mb-4 bg-blue-50/50 dark:bg-blue-500/5 p-4 rounded-xl border border-blue-100 dark:border-blue-500/10">
|
||||
<Label className="text-blue-600 dark:text-blue-400 text-xs font-bold uppercase flex items-center gap-2 mb-2">
|
||||
<Search className="w-3 h-3" />
|
||||
Adicionar Item do Catálogo
|
||||
</Label>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="text-blue-600 dark:text-blue-400 text-xs font-bold uppercase flex items-center gap-2">
|
||||
<Search className="w-3 h-3" />
|
||||
Adicionar Item do Catálogo
|
||||
</Label>
|
||||
<div className="flex bg-slate-100 dark:bg-slate-800 p-0.5 rounded-lg border border-slate-200 dark:border-slate-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatalogType('servico')}
|
||||
className={cn(
|
||||
"px-3 py-1 text-[10px] font-bold rounded-md transition-all",
|
||||
catalogType === 'servico'
|
||||
? "bg-white dark:bg-slate-700 text-blue-600 dark:text-blue-400 shadow-sm"
|
||||
: "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
|
||||
)}
|
||||
>
|
||||
SERVIÇO
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCatalogType('produto')}
|
||||
className={cn(
|
||||
"px-3 py-1 text-[10px] font-bold rounded-md transition-all",
|
||||
catalogType === 'produto'
|
||||
? "bg-white dark:bg-slate-700 text-blue-600 dark:text-blue-400 shadow-sm"
|
||||
: "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
|
||||
)}
|
||||
>
|
||||
PRODUTO
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Digite para buscar um serviço ou produto..."
|
||||
|
|
@ -980,11 +1010,10 @@ export const EntradasPlanejadasView = () => {
|
|||
/>
|
||||
</TableCell>
|
||||
<TableCell className="py-2">
|
||||
<Input
|
||||
type="number"
|
||||
<CurrencyInputV2
|
||||
className="h-9 text-right border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800"
|
||||
value={item.preco}
|
||||
onChange={(e) => handleItemChange(index, 'preco', e.target.value)}
|
||||
onChange={(val) => handleItemChange(index, 'preco', val || 0)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="py-2 text-right">
|
||||
|
|
@ -1033,11 +1062,10 @@ export const EntradasPlanejadasView = () => {
|
|||
<span className="text-slate-500 font-medium">Desconto:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-slate-400 font-medium font-bold">-</span>
|
||||
<Input
|
||||
type="number"
|
||||
<CurrencyInputV2
|
||||
className="h-8 w-24 text-right font-bold text-red-500 border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900"
|
||||
value={formData.desconto}
|
||||
onChange={(e) => setFormData({...formData, desconto: e.target.value})}
|
||||
onChange={(val) => setFormData({...formData, desconto: val || 0})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||
import { registrationSchema } from './schema';
|
||||
import { submitDriverRegistration } from '../../services/driverService';
|
||||
import { updateDriverRegistration, submitInternalDriverRegistration, getBases } from '../../services/grService';
|
||||
import { handleGrError } from '../../utils/grErrorHandler';
|
||||
import { useGrStore } from '../../services/useGrStore';
|
||||
import { toast } from 'sonner';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
|
@ -30,6 +31,8 @@ const VEHICLE_TYPES = [
|
|||
{ value: 'van', label: 'Van' },
|
||||
{ value: 'vuc', label: 'VUC (Veículo Urbano de Carga)' },
|
||||
{ value: 'caminhao', label: 'Caminhão' },
|
||||
{ value: 'cavalo', label: 'Cavalo' },
|
||||
{ value: 'carreta', label: 'Carreta' },
|
||||
{ value: 'moto', label: 'Moto' },
|
||||
];
|
||||
|
||||
|
|
@ -69,7 +72,7 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
const [submitted, setSubmitted] = useState(false);
|
||||
const [filePreviews, setFilePreviews] = useState({});
|
||||
|
||||
const { register, handleSubmit, watch, setValue, trigger, control, formState: { errors }, reset } = useForm({
|
||||
const { register, handleSubmit, watch, setValue, trigger, control, setError, formState: { errors }, reset } = useForm({
|
||||
resolver: zodResolver(registrationSchema),
|
||||
defaultValues: {
|
||||
forma_trab: '',
|
||||
|
|
@ -79,7 +82,8 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
terms: false,
|
||||
coordenador: '',
|
||||
modalidade: '',
|
||||
base: ''
|
||||
base: '',
|
||||
is_internal: isInternal
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -219,7 +223,7 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
|
||||
if (!currentUser?.base) return basesList || [];
|
||||
const userAllowedBases = currentUser.base.split(',').map(b => b.trim());
|
||||
const filtered = basesList.filter(b => userAllowedBases.includes(b.base || b.nome));
|
||||
const filtered = basesList.filter(b => userAllowedBases.includes((b.base || b.nome)?.trim()));
|
||||
console.log('GR [FILTER]: Bases filtradas:', filtered);
|
||||
return filtered;
|
||||
}, [basesList, currentUser]);
|
||||
|
|
@ -272,11 +276,6 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
// Validate common fields first
|
||||
fieldsToValidate = ['forma_trabalho', 'nome_completo', 'data_nascimento', 'cpf', 'telefone', 'cnh', 'validade_cnh', 'email'];
|
||||
|
||||
// Add internal-only mandatory fields
|
||||
if (isInternal) {
|
||||
fieldsToValidate.push('coordenador', 'modalidade');
|
||||
}
|
||||
|
||||
if (watch('forma_trabalho') === 'proprio') {
|
||||
fieldsToValidate.push('tipo_veiculo', 'placa_veiculo');
|
||||
}
|
||||
|
|
@ -284,7 +283,6 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
const requiredFiles = ['cnh_verso', 'cnh_frente', 'comprovante_residencia', 'cartao_cnpj', 'crlv'];
|
||||
if (watch('is_third_party_vehicle')) requiredFiles.push('owner_cnh_file');
|
||||
|
||||
// Valida se o arquivo foi anexado OU se já existe um preview (para casos de edição)
|
||||
const missingFiles = requiredFiles.filter(field => {
|
||||
const hasFile = watch(field) && watch(field).length > 0;
|
||||
const hasPreview = !!filePreviews[field];
|
||||
|
|
@ -300,11 +298,36 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
}
|
||||
|
||||
const isValid = await trigger(fieldsToValidate);
|
||||
|
||||
// Validação manual dos campos internos obrigatórios (superRefine não roda no trigger parcial)
|
||||
if (isInternal && step === 1) {
|
||||
let hasInternalErrors = false;
|
||||
|
||||
if (!watch('coordenador') || watch('coordenador').trim() === '') {
|
||||
setError('coordenador', { type: 'manual', message: 'Nome do Coordenador é obrigatório' });
|
||||
hasInternalErrors = true;
|
||||
}
|
||||
if (!watch('modalidade') || watch('modalidade').trim() === '') {
|
||||
setError('modalidade', { type: 'manual', message: 'Modalidade é obrigatória' });
|
||||
hasInternalErrors = true;
|
||||
}
|
||||
if (!watch('base') || watch('base').trim() === '') {
|
||||
setError('base', { type: 'manual', message: 'Base de Operação é obrigatória' });
|
||||
hasInternalErrors = true;
|
||||
}
|
||||
|
||||
if (hasInternalErrors) {
|
||||
toast.error("Por favor, preencha todos os campos obrigatórios.", {
|
||||
description: "Verifique os avisos em vermelho no formulário."
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid || step === 2) {
|
||||
setStep(prev => prev + 1);
|
||||
window.scrollTo(0, 0);
|
||||
} else {
|
||||
// Feedback de campos faltando para o usuário
|
||||
const missingFields = fieldsToValidate.filter(field => !!errors[field]);
|
||||
if (missingFields.length > 0) {
|
||||
toast.error("Por favor, preencha todos os campos obrigatórios.", {
|
||||
|
|
@ -314,6 +337,7 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
// Funções de máscara
|
||||
const maskCPF = (value) => value.replace(/\D/g, '').replace(/(\d{3})(\d)/, '$1.$2').replace(/(\d{3})(\d)/, '$1.$2').replace(/(\d{3})(\d{1,2})$/, '$1-$2').slice(0, 14);
|
||||
const maskPhone = (value) => {
|
||||
|
|
@ -460,15 +484,8 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Erro CRÍTICO na submissão:", error);
|
||||
toast.error("Não foi possível concluir a ação", {
|
||||
description: "Ocorreu um erro inesperado. Por favor, tente novamente ou entre em contato com o suporte técnico.",
|
||||
action: {
|
||||
label: "Tentar Novamente",
|
||||
onClick: () => onSubmit(data),
|
||||
},
|
||||
duration: 5000
|
||||
});
|
||||
// Uso do handler centralizado para garantir consistência e redirecionamento
|
||||
handleGrError(error, "Não foi possível concluir o cadastro.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
|
@ -715,7 +732,7 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
</div>
|
||||
{isInternal && (
|
||||
<div className="space-y-3">
|
||||
<Label className="text-xs font-bold uppercase tracking-widest text-slate-400">Modalidade</Label>
|
||||
<Label className="text-xs font-bold uppercase tracking-widest text-slate-400">Modalidade <span className="text-red-500">*</span></Label>
|
||||
<Select onValueChange={(val) => setValue('modalidade', val)} value={watch('modalidade')}>
|
||||
<SelectTrigger className={cn("h-14 rounded-2xl bg-slate-100 dark:bg-black/20 border-slate-200 dark:border-white/10 shadow-sm", errors.modalidade && "border-red-500")}>
|
||||
<SelectValue placeholder="Selecione..." />
|
||||
|
|
@ -731,12 +748,12 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
{isInternal && (
|
||||
<>
|
||||
<div className="md:col-span-2 space-y-3">
|
||||
<Label className="text-xs font-bold uppercase tracking-widest text-slate-400">Nome Coordenador</Label>
|
||||
<Label className="text-xs font-bold uppercase tracking-widest text-slate-400">Nome Coordenador <span className="text-red-500">*</span></Label>
|
||||
<Input {...register('coordenador')} className={cn("h-14 rounded-2xl bg-slate-100 dark:bg-black/20 shadow-sm", errors.coordenador && "border-red-500")} />
|
||||
{errors.coordenador && <span className="text-[10px] text-red-500 font-bold uppercase">{errors.coordenador.message}</span>}
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-3">
|
||||
<Label className="text-xs font-bold uppercase tracking-widest text-slate-400">Base de Operação</Label>
|
||||
<Label className="text-xs font-bold uppercase tracking-widest text-slate-400">Base de Operação <span className="text-red-500">*</span></Label>
|
||||
<Controller
|
||||
name="base"
|
||||
control={control}
|
||||
|
|
@ -745,18 +762,13 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
<Select
|
||||
onValueChange={(val) => {
|
||||
console.log('GR [SELECT]: Base selecionada:', val);
|
||||
console.log('GR [SELECT]: Valor atual do field:', field.value);
|
||||
// Only update if value is not empty and different from current
|
||||
if (val && val !== field.value) {
|
||||
field.onChange(val);
|
||||
console.log('GR [SELECT]: field.onChange chamado. Valor:', val);
|
||||
} else {
|
||||
console.log('GR [SELECT]: onChange ignorado (valor vazio ou igual)');
|
||||
}
|
||||
}}
|
||||
value={field.value || ""}
|
||||
>
|
||||
<SelectTrigger className="h-14 rounded-2xl bg-slate-100 dark:bg-black/20 border-slate-200 dark:border-white/10 shadow-sm">
|
||||
<SelectTrigger className={cn("h-14 rounded-2xl bg-slate-100 dark:bg-black/20 border-slate-200 dark:border-white/10 shadow-sm", errors.base && "border-red-500")}>
|
||||
<SelectValue placeholder="Selecione a base..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-2xl border-slate-200 dark:border-white/10 bg-white dark:bg-[#151515] shadow-2xl max-h-[200px]">
|
||||
|
|
@ -772,6 +784,7 @@ export const DriverRegistrationForm = ({ onSuccess, isInternal = false }) => {
|
|||
</Select>
|
||||
)}
|
||||
/>
|
||||
{errors.base && <span className="text-[10px] text-red-500 font-bold uppercase">{errors.base.message}</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const registrationSchema = z.object({
|
|||
modalidade: z.string().optional(),
|
||||
email: z.string().email("Email inválido"),
|
||||
base: z.string().optional(),
|
||||
is_internal: z.boolean().optional(),
|
||||
|
||||
// Etapa 3
|
||||
cnpj: z.string().optional(),
|
||||
|
|
@ -49,6 +50,31 @@ export const registrationSchema = z.object({
|
|||
is_third_party_vehicle: z.boolean().optional(),
|
||||
terms: z.literal(true, { errorMap: () => ({ message: "Você deve aceitar os termos" }) }),
|
||||
}).superRefine((data, ctx) => {
|
||||
// Validação restrita para cadastros internos
|
||||
if (data.is_internal) {
|
||||
if (!data.coordenador || data.coordenador.trim() === '') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Nome do Coordenador é obrigatório",
|
||||
path: ['coordenador'],
|
||||
});
|
||||
}
|
||||
if (!data.modalidade || data.modalidade.trim() === '') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Modalidade é obrigatória",
|
||||
path: ['modalidade'],
|
||||
});
|
||||
}
|
||||
if (!data.base || data.base.trim() === '') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Base de Operação é obrigatória",
|
||||
path: ['base'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.forma_trabalho === 'proprio') {
|
||||
if (!data.tipo_veiculo) {
|
||||
ctx.addIssue({
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ export const GrKanbanBoard = ({
|
|||
{/* Cards Area */}
|
||||
<div className="flex-1 px-3 py-4 overflow-y-auto custom-scrollbar min-h-0">
|
||||
<div className="flex flex-col gap-4 pb-2">
|
||||
{safeCards.map((card) => (
|
||||
<div key={card.id} className="group relative">
|
||||
{safeCards.map((card, idx) => (
|
||||
<div key={`${card.id}-${idx}`} className="group relative">
|
||||
<GrKanbanCard
|
||||
item={card}
|
||||
title={card.title}
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ export const GrKanbanCard = ({
|
|||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<Calendar size={10} className="mb-0.5" />
|
||||
<span className="whitespace-nowrap">
|
||||
{date && date.includes('00:00:00') ? date.split(' ')[0] : date}
|
||||
{typeof date === 'string' && date.includes('00:00:00') ? date.split(' ')[0] : date}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -86,10 +86,10 @@ export const GrRegistrationDetails = ({
|
|||
return bases;
|
||||
}
|
||||
|
||||
if (!currentUser?.base) return [];
|
||||
if (!currentUser?.base) return bases || [];
|
||||
|
||||
const userAllowedBases = currentUser.base.split(',').map(b => b.trim());
|
||||
return bases.filter(b => userAllowedBases.includes(b.base || b.nome));
|
||||
return bases.filter(b => userAllowedBases.includes((b.base || b.nome)?.trim()));
|
||||
}, [bases, currentUser]);
|
||||
|
||||
// Determine available documents based on real API fields
|
||||
|
|
@ -475,6 +475,8 @@ export const GrRegistrationDetails = ({
|
|||
<option value="carro_passeio" className="bg-white dark:bg-[#1a1a1a]">Carro de Passeio</option>
|
||||
<option value="utilitario" className="bg-white dark:bg-[#1a1a1a]">Utilitário</option>
|
||||
<option value="caminhao" className="bg-white dark:bg-[#1a1a1a]">Caminhão</option>
|
||||
<option value="cavalo" className="bg-white dark:bg-[#1a1a1a]">Cavalo</option>
|
||||
<option value="carreta" className="bg-white dark:bg-[#1a1a1a]">Carreta</option>
|
||||
<option value="moto" className="bg-white dark:bg-[#1a1a1a]">Moto</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { Search, Plus, X } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { handleGrError } from '../utils/grErrorHandler';
|
||||
import ExcelTable from '../components/ExcelTable';
|
||||
|
||||
|
||||
import { GrRegistrationDetails } from '../components/GrRegistrationDetails';
|
||||
import { useGrStore } from '../services/useGrStore';
|
||||
import * as grService from '../services/grService';
|
||||
|
|
@ -38,8 +41,7 @@ const GrRegistrationsSimplified = () => {
|
|||
const driverData = response.Base_Dados_API || response.dados || response;
|
||||
setData(Array.isArray(driverData) ? driverData : (driverData.dados || []));
|
||||
} catch (error) {
|
||||
console.error("Error fetching GR data:", error);
|
||||
toast.error("Erro ao carregar cadastros");
|
||||
handleGrError(error, "Erro ao carregar cadastros");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -155,8 +157,8 @@ const GrRegistrationsSimplified = () => {
|
|||
toast.success('Cadastro arquivado com sucesso');
|
||||
fetchData();
|
||||
|
||||
}).catch(() => {
|
||||
toast.error('Erro ao excluir cadastro');
|
||||
}).catch((error) => {
|
||||
handleGrError(error, 'Erro ao excluir cadastro');
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
@ -199,11 +201,7 @@ const GrRegistrationsSimplified = () => {
|
|||
fetchData();
|
||||
setSelectedCard(null);
|
||||
} catch (error) {
|
||||
console.error("Error saving GR registration:", error);
|
||||
toast.error("Não foi possível salvar as alterações", {
|
||||
description: "Verifique sua conexão e tente novamente. Se o problema persistir, contate o suporte.",
|
||||
duration: 5000
|
||||
});
|
||||
handleGrError(error, "Não foi possível salvar as alterações");
|
||||
}
|
||||
}}
|
||||
onProgressStatus={() => {}}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const GrUserForm = ({
|
|||
|
||||
{/* 1. Nome Completo */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Nome</Label>
|
||||
<Label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Nome <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
value={formData.nome_completo}
|
||||
onChange={(e) => handleInputChange('nome_completo', e.target.value)}
|
||||
|
|
@ -45,7 +45,7 @@ export const GrUserForm = ({
|
|||
|
||||
{/* 2. Email */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Email</Label>
|
||||
<Label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Email <span className="text-red-500">*</span></Label>
|
||||
<Input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
|
|
@ -109,7 +109,7 @@ export const GrUserForm = ({
|
|||
{/* 6. Base (Oculto se for Coordenador) */}
|
||||
{showBaseField && (
|
||||
<div className="space-y-2 animate-in slide-in-from-top-2 duration-300">
|
||||
<Label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Bases (Múltipla Seleção)</Label>
|
||||
<Label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Bases (Múltipla Seleção) <span className="text-red-500">*</span></Label>
|
||||
<div className="p-4 rounded-2xl bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 shadow-sm max-h-[200px] overflow-y-auto space-y-2 custom-scrollbar">
|
||||
{(() => {
|
||||
const masterBaseNames = new Set(bases.map(b => b.base || b.nome));
|
||||
|
|
@ -158,10 +158,11 @@ export const GrUserForm = ({
|
|||
|
||||
{/* 7. Responsável de Base (Pode ser dependente da escolha da base no futuro, mantendo simples por enquanto) */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Responsável de Base</Label>
|
||||
<Label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Responsável de Base <span className="text-red-500">*</span></Label>
|
||||
<Select
|
||||
value={formData.responsavel_base}
|
||||
onValueChange={(val) => handleInputChange('responsavel_base', val)}
|
||||
required
|
||||
>
|
||||
<SelectTrigger className="h-12 rounded-2xl bg-white dark:bg-white/5 border-slate-200 dark:border-white/10 shadow-sm transition-all focus:ring-2 focus:ring-[var(--gr-primary)]/20">
|
||||
<SelectValue placeholder="Selecione o responsável..." />
|
||||
|
|
@ -176,6 +177,28 @@ export const GrUserForm = ({
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 8. Modalidade - Comentado conforme solicitado por não ser usado aqui */}
|
||||
{/*
|
||||
<div className="space-y-2">
|
||||
<Label className="text-[10px] font-bold uppercase tracking-widest text-slate-400 ml-1">Modalidade <span className="text-red-500">*</span></Label>
|
||||
<Select
|
||||
value={formData.modalidade}
|
||||
onValueChange={(val) => handleInputChange('modalidade', val)}
|
||||
required
|
||||
>
|
||||
<SelectTrigger className="h-12 rounded-2xl bg-white dark:bg-white/5 border-slate-200 dark:border-white/10 shadow-sm transition-all focus:ring-2 focus:ring-[var(--gr-primary)]/20">
|
||||
<SelectValue placeholder="Selecione a modalidade..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-2xl border-slate-200 dark:border-white/10">
|
||||
<SelectItem value="Agregado">Agregado</SelectItem>
|
||||
<SelectItem value="Rentals">Rentals</SelectItem>
|
||||
<SelectItem value="Frota Fixa Diarista">Frota Fixa Diarista</SelectItem>
|
||||
<SelectItem value="Frota Fixa Mensal">Frota Fixa Mensal</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
*/}
|
||||
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ export const VehicleFormModal = ({ isOpen, onClose, onSave }) => {
|
|||
<option value="TRUCK">Caminhão</option>
|
||||
<option value="VAN">Van / Fiorino</option>
|
||||
<option value="CAR">Carro / Passeio</option>
|
||||
<option value="CAVALO">Cavalo</option>
|
||||
<option value="CARRETA">Carreta</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
|
|
|
|||
|
|
@ -14,17 +14,7 @@ export const submitDriverRegistration = async (formData) => {
|
|||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response?.data?.detail) {
|
||||
// Tenta formatar erros de validação do Pydantic/FastAPI se vierem como lista
|
||||
if (Array.isArray(error.response.data.detail)) {
|
||||
const formattedErrors = error.response.data.detail
|
||||
.map(err => `${err.loc[1]}: ${err.msg}`)
|
||||
.join('\n');
|
||||
throw new Error(formattedErrors);
|
||||
}
|
||||
throw new Error(JSON.stringify(error.response.data.detail));
|
||||
}
|
||||
throw new Error(error.message || 'Erro ao comunicar com o servidor.');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -43,9 +33,6 @@ export const uploadDriverFiles = async (submissionId, fileFormData) => {
|
|||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response?.data?.detail) {
|
||||
throw new Error(JSON.stringify(error.response.data.detail));
|
||||
}
|
||||
throw new Error(error.message || 'Erro ao enviar documentos.');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,12 +5,8 @@ import api from '@/services/api';
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getGrEnvironment = async () => {
|
||||
try {
|
||||
const response = await api.get('/mounting_gr');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao montar ambiente GR');
|
||||
}
|
||||
const response = await api.get('/mounting_gr');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -19,12 +15,8 @@ export const getGrEnvironment = async () => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getDriversByFilter = async (params) => {
|
||||
try {
|
||||
const response = await api.post('/base/filtro', params);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar motoristas');
|
||||
}
|
||||
const response = await api.post('/base/filtro', params);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -33,16 +25,12 @@ export const getDriversByFilter = async (params) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const updateDriverStatus = async (data) => {
|
||||
try {
|
||||
const response = await api.request({
|
||||
url: '/cadastro/drivers/edit/status',
|
||||
method: 'UPDATE',
|
||||
data: data
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao atualizar status');
|
||||
}
|
||||
const response = await api.request({
|
||||
url: '/cadastro/drivers/edit/status',
|
||||
method: 'UPDATE',
|
||||
data: data
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -51,12 +39,8 @@ export const updateDriverStatus = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getDriverContracts = async (base) => {
|
||||
try {
|
||||
const response = await api.get(`/contrato/drivers/filtro?base=${base}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar contratos');
|
||||
}
|
||||
const response = await api.get(`/contrato/drivers/filtro?base=${base}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -65,15 +49,11 @@ export const getDriverContracts = async (base) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getAllDriverRegistrations = async (modalidade = null) => {
|
||||
try {
|
||||
const url = modalidade
|
||||
? `/cadastro/drivers/apresentar/modalidade/${modalidade}`
|
||||
: '/cadastro/drivers/apresentar';
|
||||
const response = await api.get(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar cadastros');
|
||||
}
|
||||
const url = modalidade
|
||||
? `/cadastro/drivers/apresentar/modalidade/${modalidade}`
|
||||
: '/cadastro/drivers/apresentar';
|
||||
const response = await api.get(url);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -82,12 +62,8 @@ export const getAllDriverRegistrations = async (modalidade = null) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getArchivedRegistrations = async () => {
|
||||
try {
|
||||
const response = await api.get('/cadastro/drivers/arquivados');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar cadastros arquivados');
|
||||
}
|
||||
const response = await api.get('/cadastro/drivers/arquivados');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -96,15 +72,11 @@ export const getArchivedRegistrations = async () => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const updateDriverRegistration = async (data) => {
|
||||
try {
|
||||
const isFormData = data instanceof FormData;
|
||||
const response = await api.put('/cadastro/drivers/edit', data, {
|
||||
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : undefined
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao salvar alterações do motorista');
|
||||
}
|
||||
const isFormData = data instanceof FormData;
|
||||
const response = await api.put('/cadastro/drivers/edit', data, {
|
||||
headers: isFormData ? { 'Content-Type': 'multipart/form-data' } : undefined
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -113,15 +85,11 @@ export const updateDriverRegistration = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getActiveContracts = async (modalidade = null) => {
|
||||
try {
|
||||
const url = modalidade
|
||||
? `/contrato/drivers/apresentar?modalidade=${modalidade}`
|
||||
: '/contrato/drivers/apresentar';
|
||||
const response = await api.get(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar contratos');
|
||||
}
|
||||
const url = modalidade
|
||||
? `/contrato/drivers/apresentar?modalidade=${modalidade}`
|
||||
: '/contrato/drivers/apresentar';
|
||||
const response = await api.get(url);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -130,12 +98,8 @@ export const getActiveContracts = async (modalidade = null) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getArchivedContracts = async () => {
|
||||
try {
|
||||
const response = await api.get('/contrato/drivers/arquivados');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar contratos arquivados');
|
||||
}
|
||||
const response = await api.get('/contrato/drivers/arquivados');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -144,16 +108,12 @@ export const getArchivedContracts = async () => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const updateContractGlobalStatus = async (data) => {
|
||||
try {
|
||||
const response = await api.request({
|
||||
url: '/contrato/drivers/edit/status_global',
|
||||
method: 'UPDATE',
|
||||
data: data
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao atualizar status global do contrato');
|
||||
}
|
||||
const response = await api.request({
|
||||
url: '/contrato/drivers/edit/status_global',
|
||||
method: 'UPDATE',
|
||||
data: data
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -162,12 +122,8 @@ export const updateContractGlobalStatus = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const updateContractSignatureStatus = async (data) => {
|
||||
try {
|
||||
const response = await api.put('/contrato/drivers/edit/status_assinatura', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao atualizar status de assinatura');
|
||||
}
|
||||
const response = await api.put('/contrato/drivers/edit/status_assinatura', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -175,12 +131,8 @@ export const updateContractSignatureStatus = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getUserList = async () => {
|
||||
try {
|
||||
const response = await api.get('/usuarios_gr');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar usuários');
|
||||
}
|
||||
const response = await api.get('/usuarios_gr');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -189,12 +141,8 @@ export const getUserList = async () => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const saveUser = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/user_gr/edit', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao salvar usuário');
|
||||
}
|
||||
const response = await api.post('/user_gr/edit', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -203,12 +151,8 @@ export const saveUser = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const deleteUser = async (userId) => {
|
||||
try {
|
||||
const response = await api.delete('/user_gr/delete', { data: { idusuarios_pralog: userId } });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao deletar usuário');
|
||||
}
|
||||
const response = await api.delete('/user_gr/delete', { data: { idusuarios_pralog: userId } });
|
||||
return response.data;
|
||||
};
|
||||
/**
|
||||
* Cria um novo contrato.
|
||||
|
|
@ -216,12 +160,8 @@ export const deleteUser = async (userId) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const createContract = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/contrato/drivers', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao criar contrato');
|
||||
}
|
||||
const response = await api.post('/contrato/drivers', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -231,12 +171,8 @@ export const createContract = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const updateContract = async (data) => {
|
||||
try {
|
||||
const response = await api.put('/contrato/drivers/edit', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao atualizar contrato');
|
||||
}
|
||||
const response = await api.put('/contrato/drivers/edit', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -246,15 +182,11 @@ export const updateContract = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const archiveContract = async (idcontrato_drivers) => {
|
||||
try {
|
||||
if (!idcontrato_drivers) {
|
||||
throw new Error('ID do contrato (idcontrato_drivers) não informado para arquivamento.');
|
||||
}
|
||||
const response = await api.put(`/contrato/arquivar/${idcontrato_drivers}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao arquivar contrato');
|
||||
if (!idcontrato_drivers) {
|
||||
throw new Error('ID do contrato (idcontrato_drivers) não informado para arquivamento.');
|
||||
}
|
||||
const response = await api.put(`/contrato/arquivar/${idcontrato_drivers}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -263,13 +195,8 @@ export const archiveContract = async (idcontrato_drivers) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const createUser = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/usuarios_gr/create', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// Lançar o erro original do axios para preservar error.response.data
|
||||
throw error;
|
||||
}
|
||||
const response = await api.post('/usuarios_gr/create', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -278,13 +205,9 @@ export const createUser = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const updateUser = async (data) => {
|
||||
try {
|
||||
// Note: User requested /contrato/drivers/edit for editing users as well
|
||||
const response = await api.post('/user_gr/edit', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao atualizar usuário');
|
||||
}
|
||||
// Note: User requested /contrato/drivers/edit for editing users as well
|
||||
const response = await api.post('/user_gr/edit', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -293,12 +216,8 @@ export const updateUser = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getGrGroups = async () => {
|
||||
try {
|
||||
const response = await api.get('/grup_gr');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar grupos');
|
||||
}
|
||||
const response = await api.get('/grup_gr');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -309,15 +228,11 @@ export const getGrGroups = async () => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const archiveDriverRegistration = async (iddrivers) => {
|
||||
try {
|
||||
if (!iddrivers) {
|
||||
throw new Error('ID do cadastro não informado para arquivamento.');
|
||||
}
|
||||
const response = await api.put(`/cadatro/arquivar/${iddrivers}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao arquivar cadastro');
|
||||
if (!iddrivers) {
|
||||
throw new Error('ID do cadastro não informado para arquivamento.');
|
||||
}
|
||||
const response = await api.put(`/cadatro/arquivar/${iddrivers}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -327,16 +242,12 @@ export const archiveDriverRegistration = async (iddrivers) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const submitInternalDriverRegistration = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/cadastro/drivers/interno', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao realizar cadastro interno');
|
||||
}
|
||||
const response = await api.post('/cadastro/drivers/interno', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -345,16 +256,12 @@ export const submitInternalDriverRegistration = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const updateDriverDocStatus = async (data) => {
|
||||
try {
|
||||
const response = await api.request({
|
||||
url: '/cadastro/drivers/edit/status_motorista_carro',
|
||||
method: 'UPDATE',
|
||||
data: data
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao atualizar status dos documentos');
|
||||
}
|
||||
const response = await api.request({
|
||||
url: '/cadastro/drivers/edit/status_motorista_carro',
|
||||
method: 'UPDATE',
|
||||
data: data
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -364,12 +271,8 @@ export const updateDriverDocStatus = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const saveEmailCredentials = async (email, senha) => {
|
||||
try {
|
||||
const response = await api.post('/email_xml/credenciais', { email, senha });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao salvar credenciais de e-mail');
|
||||
}
|
||||
const response = await api.post('/email_xml/credenciais', { email, senha });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -378,12 +281,8 @@ export const saveEmailCredentials = async (email, senha) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const processEmails = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/email_xml/processar', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao processar e-mails');
|
||||
}
|
||||
const response = await api.post('/email_xml/processar', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -392,18 +291,14 @@ export const processEmails = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const validateXML = async (file) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const response = await api.post('/email_xml/validar', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao validar arquivo XML');
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const response = await api.post('/email_xml/validar', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -414,12 +309,8 @@ export const validateXML = async (file) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const changeUserPassword = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/user_gr/senha', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao alterar senha');
|
||||
}
|
||||
const response = await api.post('/user_gr/senha', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -429,12 +320,8 @@ export const changeUserPassword = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const resetPasswordExternal = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/ambiente_python/reset_password', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao redefinir senha');
|
||||
}
|
||||
const response = await api.post('/ambiente_python/reset_password', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -444,12 +331,8 @@ export const resetPasswordExternal = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const requestPasswordReset = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/forgot_password', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao solicitar redefinição de senha');
|
||||
}
|
||||
const response = await api.post('/forgot_password', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -458,12 +341,8 @@ export const requestPasswordReset = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getBases = async () => {
|
||||
try {
|
||||
const response = await api.get('/bases/apresentar');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar bases');
|
||||
}
|
||||
const response = await api.get('/bases/apresentar');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -474,12 +353,8 @@ export const getBases = async () => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const saveBase = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/bases', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao criar base');
|
||||
}
|
||||
const response = await api.post('/bases', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -489,16 +364,12 @@ export const saveBase = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const updateBase = async (data) => {
|
||||
try {
|
||||
const payload = {
|
||||
idbase: data.idbase,
|
||||
base: data.base
|
||||
};
|
||||
const response = await api.put('/bases/editar', payload);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao atualizar base');
|
||||
}
|
||||
const payload = {
|
||||
idbase: data.idbase,
|
||||
base: data.base
|
||||
};
|
||||
const response = await api.put('/bases/editar', payload);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -508,12 +379,8 @@ export const updateBase = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const deleteBase = async (id) => {
|
||||
try {
|
||||
const response = await api.delete('/bases/excluir', { data: { idbase: id } });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao deletar base');
|
||||
}
|
||||
const response = await api.delete('/bases/excluir', { data: { idbase: id } });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -522,12 +389,8 @@ export const deleteBase = async (id) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const getBaseResponsibles = async () => {
|
||||
try {
|
||||
const response = await api.get('/responsavel_bases');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao buscar responsáveis');
|
||||
}
|
||||
const response = await api.get('/responsavel_bases');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -538,12 +401,8 @@ export const getBaseResponsibles = async () => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const saveBaseResponsible = async (data) => {
|
||||
try {
|
||||
const response = await api.post('/responsavel_bases', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao criar responsável');
|
||||
}
|
||||
const response = await api.post('/responsavel_bases', data);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -554,16 +413,12 @@ export const saveBaseResponsible = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const updateBaseResponsible = async (data) => {
|
||||
try {
|
||||
const payload = {
|
||||
id_responsavel_base: data.id_responsavel_base,
|
||||
responsavel_base: data.responsavel_base
|
||||
};
|
||||
const response = await api.put('/responsavel_bases/editar', payload);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao atualizar responsável');
|
||||
}
|
||||
const payload = {
|
||||
id_responsavel_base: data.id_responsavel_base,
|
||||
responsavel_base: data.responsavel_base
|
||||
};
|
||||
const response = await api.put('/responsavel_bases/editar', payload);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -573,12 +428,8 @@ export const updateBaseResponsible = async (data) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const deleteBaseResponsible = async (id) => {
|
||||
try {
|
||||
const response = await api.delete('/responsavel_bases/excluir', { data: { id_responsavel_base: id } });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao deletar responsável');
|
||||
}
|
||||
const response = await api.delete('/responsavel_bases/excluir', { data: { id_responsavel_base: id } });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -588,15 +439,11 @@ export const deleteBaseResponsible = async (id) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const unarchiveDriverRegistration = async (iddrivers) => {
|
||||
try {
|
||||
if (!iddrivers) {
|
||||
throw new Error('ID do cadastro não informado para desarquivamento.');
|
||||
}
|
||||
const response = await api.put(`/cadatro/desarquivar/${iddrivers}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(error.response?.data?.message || 'Erro ao desarquivar cadastro');
|
||||
if (!iddrivers) {
|
||||
throw new Error('ID do cadastro não informado para desarquivamento.');
|
||||
}
|
||||
const response = await api.put(`/cadatro/desarquivar/${iddrivers}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -606,14 +453,9 @@ export const unarchiveDriverRegistration = async (iddrivers) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const unarchiveContract = async (id) => {
|
||||
try {
|
||||
if (!id) {
|
||||
throw new Error('ID do contrato não informado para desarquivamento.');
|
||||
}
|
||||
const response = await api.put(`/contrato/desarquivar/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
const errorMsg = error.response?.data?.message || error.message || 'Erro ao desarquivar contrato';
|
||||
throw new Error(errorMsg);
|
||||
if (!id) {
|
||||
throw new Error('ID do contrato não informado para desarquivamento.');
|
||||
}
|
||||
const response = await api.put(`/contrato/desarquivar/${id}`);
|
||||
return response.data;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,54 +10,90 @@ export const handleGrError = (error, defaultMessage = "Ocorreu um erro inesperad
|
|||
console.error("GR [ERROR]:", error);
|
||||
|
||||
let message = defaultMessage;
|
||||
let description = "";
|
||||
let status = error.response?.status;
|
||||
|
||||
// Check for specific error response structure from axios/api
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const data = error.response.data;
|
||||
|
||||
// Prioritize message from backend if available
|
||||
// Backend retorna erro em português no campo 'erro' (ex: {erro: "Nenhuma base encontrada..."})
|
||||
if (data && data.erro) {
|
||||
message = data.erro;
|
||||
} else if (data && data.message) {
|
||||
message = data.message;
|
||||
} else if (data && data.error) {
|
||||
message = data.error;
|
||||
}
|
||||
|
||||
// Handle specific status codes if message is generic or missing
|
||||
if (!message || message === defaultMessage) {
|
||||
switch (status) {
|
||||
case 400:
|
||||
message = "Dados inválidos. Verifique as informações preenchidas.";
|
||||
break;
|
||||
case 401:
|
||||
message = "Sessão expirada. Faça login novamente.";
|
||||
break;
|
||||
case 403:
|
||||
message = "Você não tem permissão para realizar esta ação.";
|
||||
break;
|
||||
case 404:
|
||||
message = "Recurso não encontrado.";
|
||||
break;
|
||||
case 500:
|
||||
message = "Erro Interno (500). Por favor, contate o suporte técnico.";
|
||||
break;
|
||||
case 502:
|
||||
message = "Serviço Indisponível (502). O sistema está temporariamente fora do ar.";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (error.message) {
|
||||
// Handle network errors or other JS errors
|
||||
message = error.message;
|
||||
// 1. Redirecionamento para 401 e 403 (Segurança)
|
||||
if (status === 401 || status === 403) {
|
||||
toast.error("Sessão expirada ou acesso negado", {
|
||||
description: "Por segurança, você será redirecionado para a tela de login."
|
||||
});
|
||||
|
||||
// Pequeno delay para o usuário ler a mensagem antes do redirect
|
||||
setTimeout(() => {
|
||||
window.location.href = '/plataforma/gr/login';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up message (remove prefixes like "Error: ")
|
||||
message = message.replace(/^Error:\s*/i, '');
|
||||
// 2. Extração e Sanitização da mensagem do Backend
|
||||
if (error.response?.data) {
|
||||
const data = error.response.data;
|
||||
|
||||
// Tenta extrair mensagem de vários formatos comuns (FastAPI detail, erro, message, etc)
|
||||
let rawMessage = '';
|
||||
|
||||
if (data.detail) {
|
||||
if (Array.isArray(data.detail)) {
|
||||
rawMessage = data.detail.map(d => `${d.loc[d.loc.length - 1]}: ${d.msg}`).join(', ');
|
||||
} else if (typeof data.detail === 'string') {
|
||||
rawMessage = data.detail;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rawMessage) {
|
||||
rawMessage = data.erro || data.message || data.error || '';
|
||||
}
|
||||
|
||||
// Lista de termos que indicam exposição de código ou logs técnicos
|
||||
const technicalTerms = ['sql', 'exception', 'traceback', 'internal server error', 'undefined', 'null', 'nan', '{', '[', 'stack', 'database', 'query'];
|
||||
|
||||
if (rawMessage && !technicalTerms.some(term => String(rawMessage).toLowerCase().includes(term))) {
|
||||
// Se tivermos uma mensagem amigável do backend, usamos como descrição detalhada
|
||||
description = rawMessage;
|
||||
} else if (rawMessage) {
|
||||
// Se a mensagem parecer técnica, usamos um fallback amigável
|
||||
message = "Erro no processamento";
|
||||
description = "Não foi possível completar a operação devido a um erro interno. Por favor, tente novamente.";
|
||||
}
|
||||
}
|
||||
|
||||
toast.error(message);
|
||||
// 3. Tratamento amigável por Status Code
|
||||
switch (status) {
|
||||
case 400:
|
||||
// Se não tiver descrição da sanitização, coloca uma padrão mais amigável
|
||||
if (!description) description = "Verifique se todos os campos obrigatórios foram preenchidos corretamente e tente salvar novamente.";
|
||||
break;
|
||||
case 409:
|
||||
message = "Conflito de Informações";
|
||||
if (!description) description = "Este registro já existe ou há um conflito com os dados enviados (ex: CPF já cadastrado).";
|
||||
break;
|
||||
case 404:
|
||||
message = "Informação não encontrada";
|
||||
description = "O registro ou a página que você está tentando acessar não foi localizado ou pode ter sido removido.";
|
||||
break;
|
||||
case 500:
|
||||
case 502:
|
||||
message = "Sistema em Manutenção";
|
||||
description = "O serviço está temporariamente indisponível. Nossa equipe técnica já foi notificada.";
|
||||
break;
|
||||
default:
|
||||
if (!error.response) {
|
||||
if (error.code === 'ECONNABORTED' || error.message?.toLowerCase().includes('timeout')) {
|
||||
message = "Tempo de Resposta Excedido";
|
||||
description = "O servidor demorou muito para responder. Por favor, verifique sua conexão ou tente novamente via tela de configuração.";
|
||||
} else {
|
||||
message = "Falha na Rede";
|
||||
description = "Não conseguimos conectar ao servidor. Verifique sua conexão com a internet.";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// 4. Exibição via Sonner de forma Premium
|
||||
toast.error(message, {
|
||||
description: description,
|
||||
duration: 5000,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { Search, Loader2, ArrowLeft, ArchiveRestore } from 'lucide-react';
|
||||
import ExcelTable from '../components/ExcelTable';
|
||||
import { toast } from 'sonner';
|
||||
import { handleGrError } from '../utils/grErrorHandler';
|
||||
import * as grService from '../services/grService';
|
||||
|
||||
|
||||
|
||||
const ArchivedContractsView = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
|
@ -37,8 +40,7 @@ const ArchivedContractsView = () => {
|
|||
setData(enhancedList);
|
||||
setBases(Array.isArray(basesList) ? basesList : []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching archived contracts:', error);
|
||||
toast.error(error.message ?? 'Erro ao carregar contratos arquivados.');
|
||||
handleGrError(error, "Erro ao carregar contratos arquivados.");
|
||||
setData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -67,8 +69,7 @@ const ArchivedContractsView = () => {
|
|||
toast.success('Contrato desarquivado com sucesso!');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('Error unarchiving contract:', error);
|
||||
toast.error(error.message || 'Erro ao desarquivar o contrato.');
|
||||
handleGrError(error, "Erro ao desarquivar o contrato.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { useNavigate, Navigate } from 'react-router-dom';
|
|||
import { Search, Loader2, ArrowLeft, ArchiveRestore } from 'lucide-react';
|
||||
import ExcelTable from '../components/ExcelTable';
|
||||
import { toast } from 'sonner';
|
||||
import { handleGrError } from '../utils/grErrorHandler';
|
||||
import * as grService from '../services/grService';
|
||||
|
||||
|
||||
import { useGrStore } from '../services/useGrStore';
|
||||
|
||||
const ArchivedRegistrationsView = () => {
|
||||
|
|
@ -32,8 +35,7 @@ const ArchivedRegistrationsView = () => {
|
|||
const basesList = basesResp.Base_Dados_API || basesResp.dados || basesResp || [];
|
||||
setBases(Array.isArray(basesList) ? basesList : []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching archived registrations:', error);
|
||||
toast.error(error.message ?? 'Erro ao carregar cadastros arquivados.');
|
||||
handleGrError(error, "Erro ao carregar cadastros arquivados.");
|
||||
setData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -62,11 +64,11 @@ const ArchivedRegistrationsView = () => {
|
|||
toast.success('Cadastro desarquivado com sucesso!');
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error('Error unarchiving registration:', error);
|
||||
toast.error(error.message || 'Erro ao desarquivar o cadastro.');
|
||||
handleGrError(error, "Erro ao desarquivar o cadastro.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const columns = [
|
||||
{ field: 'nome_completo', header: 'Nome', width: 200 },
|
||||
{ field: 'base', header: 'Base', width: 100 },
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import { GrKanbanBoard } from '../components/GrKanbanBoard';
|
|||
import { CadastrosAbertosColumn } from '../components/CadastrosAbertosColumn';
|
||||
import { useGrStore } from '../services/useGrStore';
|
||||
import { toast } from 'sonner';
|
||||
import { handleGrError } from '../utils/grErrorHandler';
|
||||
import { GrFormSidebar } from '../components/GrFormSidebar';
|
||||
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
|
|
@ -86,8 +89,7 @@ const ContractsView = () => {
|
|||
setArchivedContracts([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching contracts data:", error);
|
||||
toast.error("Erro ao carregar dados de contratos.");
|
||||
handleGrError(error, "Erro ao carregar dados de contratos.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -196,7 +198,7 @@ const ContractsView = () => {
|
|||
setIsSidebarOpen(false);
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
toast.error(error.message || "Erro ao salvar contrato.");
|
||||
handleGrError(error, "Erro ao salvar contrato.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -301,8 +303,7 @@ const ContractsView = () => {
|
|||
toast.success("Movimentação processada com sucesso!");
|
||||
fetchData(); // Refresh para garantir paridade com o banco
|
||||
} catch (error) {
|
||||
console.error("Error moving card:", error);
|
||||
toast.error("Erro ao atualizar status.");
|
||||
handleGrError(error, "Erro ao atualizar status.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -601,7 +602,7 @@ const ContractsView = () => {
|
|||
toast.success("Contrato criado com sucesso!");
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
toast.error(error.message || "Erro ao criar contrato.");
|
||||
handleGrError(error, "Erro ao criar contrato.");
|
||||
throw error;
|
||||
}
|
||||
}}
|
||||
|
|
@ -672,8 +673,7 @@ const ContractsView = () => {
|
|||
setIsSidebarOpen(false);
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error("Erro ao arquivar contrato:", error);
|
||||
toast.error(error.message || "Erro ao arquivar contrato.");
|
||||
handleGrError(error, "Erro ao arquivar contrato.");
|
||||
}
|
||||
}}
|
||||
className="w-full sm:w-auto sm:flex-1 py-4 bg-slate-200 hover:bg-slate-300 text-slate-800 rounded-2xl font-bold uppercase text-xs tracking-widest transition-all shadow-md shadow-slate-400/20 active:scale-95"
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import { useGrOps } from '../hooks/useGrOps';
|
|||
import * as grService from '../services/grService';
|
||||
import { useAuthContext } from '@/components/shared/AuthProvider';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { handleGrError } from '../utils/grErrorHandler';
|
||||
const GrOutlookView = () => {
|
||||
|
||||
|
||||
const { userData } = useAuthContext();
|
||||
const user = userData?.usuario || { username: '', email: '' };
|
||||
|
||||
|
|
@ -34,7 +36,7 @@ const GrOutlookView = () => {
|
|||
toast.success("Credenciais salvas com sucesso");
|
||||
setPasswordCred('');
|
||||
} catch (error) {
|
||||
toast.error(error.message || "Erro ao salvar credenciais");
|
||||
handleGrError(error, "Erro ao salvar credenciais");
|
||||
} finally {
|
||||
setIsSavingCreds(false);
|
||||
}
|
||||
|
|
@ -52,7 +54,7 @@ const GrOutlookView = () => {
|
|||
setResults(resp);
|
||||
toast.success(`${resp.total_xmls} XMLs processados com sucesso`);
|
||||
} catch (error) {
|
||||
toast.error(error.message || "Erro ao processar e-mails");
|
||||
handleGrError(error, "Erro ao processar e-mails");
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
|
|
@ -73,7 +75,7 @@ const GrOutlookView = () => {
|
|||
toast.error("XML inválido para o sistema");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(error.message || "Erro ao validar XML");
|
||||
handleGrError(error, "Erro ao validar XML");
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { Search, Plus, Filter, X, Loader2, Archive } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { handleGrError } from '../utils/grErrorHandler';
|
||||
import ExcelTable from '../components/ExcelTable';
|
||||
|
||||
|
||||
import { GrKanbanBoard } from '../components/GrKanbanBoard';
|
||||
import { GrRegistrationDetails } from '../components/GrRegistrationDetails';
|
||||
import { useGrStore } from '../services/useGrStore';
|
||||
|
|
@ -66,7 +69,7 @@ const RegistrationsView = () => {
|
|||
setContracts(Array.isArray(contractData) ? contractData : []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching GR data:", error);
|
||||
handleGrError(error, "Não foi possível carregar os cadastros.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -138,8 +141,7 @@ const RegistrationsView = () => {
|
|||
item.iddrivers === cardId ? { ...item, global_status: apiStatus } : item
|
||||
));
|
||||
} catch (error) {
|
||||
console.error("Error moving card:", error);
|
||||
alert("Erro ao atualizar status.");
|
||||
handleGrError(error, "Erro ao atualizar status.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -151,9 +153,11 @@ const RegistrationsView = () => {
|
|||
|
||||
const lowerSearch = searchTerm.toLowerCase();
|
||||
return data.filter(d =>
|
||||
d.nome_completo?.toLowerCase().includes(lowerSearch) ||
|
||||
d.cpf?.includes(searchTerm) ||
|
||||
d.placa_veiculo?.toLowerCase().includes(lowerSearch)
|
||||
d && (
|
||||
d.nome_completo?.toLowerCase().includes(lowerSearch) ||
|
||||
d.cpf?.includes(searchTerm) ||
|
||||
d.placa_veiculo?.toLowerCase().includes(lowerSearch)
|
||||
)
|
||||
);
|
||||
}, [data, searchTerm]);
|
||||
|
||||
|
|
@ -241,22 +245,24 @@ const RegistrationsView = () => {
|
|||
// Standard registration mapping for all roles
|
||||
cards = filteredData
|
||||
.filter(d => {
|
||||
if (!d) return false;
|
||||
// Blocking logic: Agregado/Rentals without signed contract only show in 'Aguardando Análise'
|
||||
const modalidade = normalizeModalidade(d.modalidade);
|
||||
const isBlocked = modalidade === 'Agregado' || modalidade === 'Rentals';
|
||||
|
||||
if (isBlocked) {
|
||||
const contract = allContracts.find(c => c.iddrivers === d.iddrivers);
|
||||
const isSigned = contract?.status_assinatura === 'Assinado' || contract?.global_status === 'Assinados';
|
||||
const hasSignedContract = contract?.status_assinatura === 'Assinado';
|
||||
|
||||
if (!isSigned) {
|
||||
return col.id === 'aguardando' && col.statusKeys?.includes(d.global_status);
|
||||
if (!hasSignedContract && col.id !== 'aguardando') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return col.statusKeys?.includes(d.global_status);
|
||||
|
||||
const currentStatus = d.global_status || d.STATUS || d.status;
|
||||
return col.statusKeys.includes(currentStatus);
|
||||
})
|
||||
.map(d => {
|
||||
.map((d, dIdx) => {
|
||||
const modalidade = normalizeModalidade(d.modalidade);
|
||||
const isBlocked = modalidade === 'Agregado' || modalidade === 'Rentals';
|
||||
const contract = allContracts.find(c => c.iddrivers === d.iddrivers);
|
||||
|
|
@ -265,11 +271,11 @@ const RegistrationsView = () => {
|
|||
|
||||
return {
|
||||
...d,
|
||||
id: d.iddrivers,
|
||||
title: d.nome_completo,
|
||||
subtitle: d.cpf,
|
||||
date: d.data_cadastro,
|
||||
base: d.base,
|
||||
id: d.iddrivers || `temp-${dIdx}`,
|
||||
title: d.nome_completo || 'Sem Nome',
|
||||
subtitle: d.cpf || 'Sem CPF',
|
||||
date: d.data_cadastro || d.data_envio || '',
|
||||
base: d.base || 'Sem Base',
|
||||
details: [ { label: 'Placa', value: d.placa_veiculo }, { label: 'Modalidade', value: d.modalidade } ],
|
||||
isBlocked: cardIsBlocked,
|
||||
status: d.global_status
|
||||
|
|
@ -352,8 +358,7 @@ const RegistrationsView = () => {
|
|||
fetchData();
|
||||
setSelectedCard(null);
|
||||
} catch (error) {
|
||||
console.error("Error saving GR registration:", error);
|
||||
alert(error.message || "Erro ao salvar alterações.");
|
||||
handleGrError(error, "Erro ao salvar alterações.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -384,8 +389,7 @@ const RegistrationsView = () => {
|
|||
fetchData();
|
||||
setSelectedCard(null);
|
||||
} catch (error) {
|
||||
console.error("Error progressing status:", error);
|
||||
alert("Erro ao avançar status.");
|
||||
handleGrError(error, "Erro ao avançar status.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -398,7 +402,7 @@ const RegistrationsView = () => {
|
|||
fetchData();
|
||||
setSelectedCard(null);
|
||||
} catch (error) {
|
||||
alert("Erro ao aprovar.");
|
||||
handleGrError(error, "Erro ao aprovar.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -411,7 +415,7 @@ const RegistrationsView = () => {
|
|||
fetchData();
|
||||
setSelectedCard(null);
|
||||
} catch (error) {
|
||||
alert("Erro ao reprovar.");
|
||||
handleGrError(error, "Erro ao reprovar.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -524,7 +528,7 @@ const RegistrationsView = () => {
|
|||
onCardClick={(card) => {
|
||||
if (userGroup === 'Despachante' || userGroup === 'Coordenador') {
|
||||
// Only 'Pendências / Revisão' allows interaction (navigation to form)
|
||||
if (card.global_status === 'Pendências / Revisão') {
|
||||
if (card.status === 'Pendências / Revisão') {
|
||||
sessionStorage.setItem('info', JSON.stringify(card));
|
||||
navigate('novo');
|
||||
} else {
|
||||
|
|
@ -584,8 +588,7 @@ const RegistrationsView = () => {
|
|||
fetchData();
|
||||
setSelectedCard(null);
|
||||
} catch (error) {
|
||||
console.error("Error archiving registration:", error);
|
||||
toast.error(error.message || "Erro ao arquivar cadastro.");
|
||||
handleGrError(error, "Erro ao arquivar cadastro.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ const SettingsView = () => {
|
|||
setor: 'Gestão de Risco', // Default as requested
|
||||
base: [], // Changed to array for multi-select
|
||||
responsavel_base: '',
|
||||
grupo: ''
|
||||
grupo: '',
|
||||
modalidade: ''
|
||||
});
|
||||
|
||||
const [baseFormData, setBaseFormData] = useState({
|
||||
|
|
@ -117,7 +118,8 @@ const SettingsView = () => {
|
|||
setor: 'Gestão de Risco',
|
||||
base: selectedBases,
|
||||
responsavel_base: user.responsavel_base || user.responsavel || '',
|
||||
grupo: user.grupo || user.group || ''
|
||||
grupo: user.grupo || user.group || '',
|
||||
modalidade: user.modalidade || ''
|
||||
});
|
||||
setIsSidebarOpen(true);
|
||||
};
|
||||
|
|
@ -131,7 +133,8 @@ const SettingsView = () => {
|
|||
setor: 'Gestão de Risco',
|
||||
base: [],
|
||||
responsavel_base: '',
|
||||
grupo: ''
|
||||
grupo: '',
|
||||
modalidade: ''
|
||||
});
|
||||
setIsSidebarOpen(true);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -141,9 +141,9 @@ export const WorkspaceLayout = () => {
|
|||
isDarkMode ? "bg-[#0a0a0a]/80 border-white/5 shadow-2xl shadow-black/20" : "bg-white/80 border-slate-200"
|
||||
)}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-9 h-9 flex items-center justify-center">
|
||||
{/* <div className="w-9 h-9 flex items-center justify-center">
|
||||
<img src={logo} alt="iTGUYS" className="h-6 w-auto" />
|
||||
</div>
|
||||
</div> */}
|
||||
<h2 className={cn(
|
||||
"text-sm font-bold uppercase tracking-[0.2em]",
|
||||
isDarkMode ? "text-white" : "text-slate-800"
|
||||
|
|
|
|||
|
|
@ -170,13 +170,15 @@ export const WorkspaceSidebar = ({ activeScreen, onScreenChange, isCollapsed, on
|
|||
|
||||
<footer className="ws-footer">
|
||||
<div className={cn("ws-brand", isCollapsed && "justify-center mb-0")}>
|
||||
<div className="ws-brand-logo">
|
||||
{/* <div className="ws-brand-logo">
|
||||
<Zap size={20} />
|
||||
</div>
|
||||
</div> */}
|
||||
<img src={logo} alt="iTGUYS" className="h-10 w-auto mb-2 drop-shadow-[0_0_15px_rgba(38,177,199,0.2)]" />
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="ws-brand-info">
|
||||
<span className="ws-brand-name">iT<span>GUYS</span></span>
|
||||
<span className="ws-app-sub">Master Core v4</span>
|
||||
{/* <span className="ws-app-sub">Master Core v4</span> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -177,6 +177,24 @@ export const boletosService = {
|
|||
const response = await api.post('/boletos/agendar/individual', payload);
|
||||
return response.data;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Busca detalhes de juros de um cliente específico
|
||||
* Rota: GET /financeiro/cliente/boletos
|
||||
* @param {number|string} idempresa
|
||||
*/
|
||||
fetchJurosCliente: (idempresa) => handleRequest({
|
||||
mockFn: () => simulateLatency([]),
|
||||
apiFn: async () => {
|
||||
const response = await api.get('/financeiro/cliente/boletos', {
|
||||
params: { idempresa }
|
||||
});
|
||||
const raw = response?.data ?? response;
|
||||
// Assume a estrutura padrão de retorno do sistema ou extraído diretamente
|
||||
const data = raw?.dados ?? raw?.Base_Dados_API ?? raw;
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -327,6 +327,85 @@ export const extratoService = {
|
|||
const response = await api.get('/financeiro/resumo_mes', { params });
|
||||
return response?.data ?? response;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Busca dados para o gráfico de planejado vs executado (Receitas)
|
||||
* Rota: GET /extrato/planejado/grafico
|
||||
* @param {Object} params - { dia, mes, ano }
|
||||
*/
|
||||
fetchPlanejadoGrafico: (params) => handleRequest({
|
||||
mockFn: () => simulateLatency({ filtro: params, grafico: [] }),
|
||||
apiFn: async () => {
|
||||
const response = await api.get('/extrato/planejado/grafico', { params });
|
||||
return response?.data ?? response;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Busca totais de despesas planejadas para o painel de cruzamento
|
||||
* Rota: GET /financeiro/despesas_mes
|
||||
* @param {Object} params - { mes, ano }
|
||||
*/
|
||||
fetchDespesasResumo: (params) => handleRequest({
|
||||
mockFn: () => simulateLatency({
|
||||
mes: Number(params.mes),
|
||||
ano: Number(params.ano),
|
||||
total_planejado: 0,
|
||||
total_realizado: 0,
|
||||
diferenca: 0
|
||||
}),
|
||||
apiFn: async () => {
|
||||
const response = await api.get('/financeiro/despesas_mes', { params });
|
||||
return response?.data ?? response;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Busca dados para o gráfico de planejado vs executado (Despesas)
|
||||
* Rota: GET /extrato/despesas/grafico
|
||||
* @param {Object} params - { dia, mes, ano }
|
||||
*/
|
||||
fetchDespesasGrafico: (params) => handleRequest({
|
||||
mockFn: () => simulateLatency({ filtro: params, grafico: [] }),
|
||||
apiFn: async () => {
|
||||
const response = await api.get('/extrato/despesas/grafico', { params });
|
||||
return response?.data ?? response;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Busca detalhamento de juros adicionados
|
||||
* Rota: GET /extrato/adicionado
|
||||
* @param {Object} params - { mes, ano }
|
||||
*/
|
||||
fetchJurosAdicionados: (params) => handleRequest({
|
||||
mockFn: () => simulateLatency({ adicionado: [], total_adicionado: 0, total_registros: 0 }),
|
||||
apiFn: async () => {
|
||||
const response = await api.get('/extrato/adicionado', { params });
|
||||
return response?.data ?? response;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Busca transações de um beneficiário específico
|
||||
* Rota: POST /beneficiario_aplicado
|
||||
* @param {string} beneficiario_pagador
|
||||
*/
|
||||
fetchBeneficiarioAplicado: (beneficiario_pagador) => handleRequest({
|
||||
mockFn: () => simulateLatency([]),
|
||||
apiFn: async () => {
|
||||
const response = await api.post('/beneficiario_aplicado', {
|
||||
beneficiario_pagador: beneficiario_pagador
|
||||
});
|
||||
const raw = response?.data ?? response;
|
||||
let data = raw?.dados ?? raw?.Base_Dados_API ?? raw;
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
data = Object.values(data || {});
|
||||
}
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ export const fornecedoresService = {
|
|||
cidade: fornecedorData.cidade || '',
|
||||
uf: fornecedorData.uf || '',
|
||||
cep: fornecedorData.cep || '',
|
||||
idregra: fornecedorData.idregra || fornecedorData.regra_id || null
|
||||
// idregra enviado como lista
|
||||
idregra: Array.isArray(fornecedorData.idregra) ? fornecedorData.idregra : (fornecedorData.idregra ? [fornecedorData.idregra] : [])
|
||||
};
|
||||
|
||||
const response = await api.post('/fornecedores/create', payload);
|
||||
|
|
@ -145,7 +146,8 @@ export const fornecedoresService = {
|
|||
cidade: fornecedorData.cidade || '',
|
||||
uf: fornecedorData.uf || '',
|
||||
cep: fornecedorData.cep || '',
|
||||
idregra: fornecedorData.idregra || fornecedorData.regra_id || null
|
||||
// idregra enviado como lista
|
||||
idregra: Array.isArray(fornecedorData.idregra) ? fornecedorData.idregra : (fornecedorData.idregra ? [fornecedorData.idregra] : [])
|
||||
};
|
||||
|
||||
const response = await api.post('/fornecedores/edit', payload);
|
||||
|
|
@ -162,17 +164,19 @@ export const fornecedoresService = {
|
|||
deleteFornecedor: (id) => handleRequest({
|
||||
mockFn: () => simulateLatency({ success: true }),
|
||||
apiFn: async () => {
|
||||
// Tenta primeiro POST /fornecedores/delete
|
||||
// Tenta primeiro POST /fornecedores/delete enviando idfornecedores no corpo (JSON)
|
||||
try {
|
||||
const response = await api.post('/fornecedores/delete', { idfornecedores: id });
|
||||
console.log('[fornecedoresService] Tentando excluir fornecedor via POST /fornecedores/delete:', id);
|
||||
const response = await api.delete('/fornecedores/delete', { idfornecedores: id });
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
console.warn('[fornecedoresService] Falha no POST /fornecedores/delete, tentando fallback DELETE:', err.message);
|
||||
// Fallback: tenta DELETE /fornecedores/:id
|
||||
try {
|
||||
const response = await api.delete(`/fornecedores/${id}`);
|
||||
return response.data;
|
||||
} catch (err2) {
|
||||
console.error('[fornecedoresService] Erro ao excluir fornecedor:', err2);
|
||||
console.error('[fornecedoresService] Erro ao excluir fornecedor (todas as rotas):', err2);
|
||||
throw err2;
|
||||
}
|
||||
}
|
||||
|
|
@ -180,18 +184,23 @@ export const fornecedoresService = {
|
|||
}),
|
||||
|
||||
/**
|
||||
* Busca movimentações aplicadas a uma regra (usado no extrato do fornecedor)
|
||||
* Busca movimentações aplicadas a uma ou mais regras (usado no extrato do fornecedor)
|
||||
* Rota: POST /regra_aplicada
|
||||
* @param {number|string} idregra - ID da regra/categoria
|
||||
* @param {number|string|Array} idregra - ID ou lista de IDs de regra
|
||||
* @param {string} tipoOperacao - 'C' ou 'D'
|
||||
* @param {Array} [idempresa] - Lista de IDs de empresa (opcional)
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
fetchRegraAplicada: (idregra, tipoOperacao) => handleRequest({
|
||||
fetchRegraAplicada: (idregra, tipoOperacao, idempresa) => handleRequest({
|
||||
mockFn: () => simulateLatency([]),
|
||||
apiFn: async () => {
|
||||
// Normaliza idregra como array
|
||||
const regraIds = Array.isArray(idregra) ? idregra : (idregra ? [idregra] : []);
|
||||
|
||||
const payload = {
|
||||
idregra: idregra,
|
||||
tipoOperacao: tipoOperacao
|
||||
idregra: regraIds,
|
||||
tipoOperacao: tipoOperacao,
|
||||
...(idempresa && Array.isArray(idempresa) && idempresa.length > 0 ? { idempresa } : {})
|
||||
};
|
||||
|
||||
const response = await api.post('/regra_aplicada', payload);
|
||||
|
|
|
|||
Loading…
Reference in New Issue