feat: novas categorias e melhorias UI transações
- Adicionadas categorias para Marco Leite: - Gastos Trabajo (com subcategorias: Máquina de Vending, Café/Snacks, etc.) - Tabaco/Vaper (com subcategorias: Cigarros, Vaper/Pod, Líquidos, etc.) - Beleza (com subcategorias: Cabeleireiro, Barbearia, Skincare, etc.) - Subscrições (com subcategorias: Streaming, Software, Cloud, etc.) - Bono Transporte (subcategoria de Transporte) - Descarga de Passivo (subcategoria de Finanças) - Lista de transações filtradas agora exibe menu completo de ações (igual à listagem por semana): editar, duplicar, dividir, etc.
This commit is contained in:
parent
d1ab280997
commit
99a68f4520
@ -228,7 +228,7 @@ public function update(Request $request, $id)
|
|||||||
$user = User::findOrFail($id);
|
$user = User::findOrFail($id);
|
||||||
|
|
||||||
// Don't allow changing main admin's admin status
|
// Don't allow changing main admin's admin status
|
||||||
if ($user->email === 'marco@cnxifly.com' && $request->has('is_admin') && !$request->is_admin) {
|
if ($user->email === 'marcoitaloesp@icloud.com' && $request->has('is_admin') && !$request->is_admin) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'No se puede remover permisos del administrador principal',
|
'message' => 'No se puede remover permisos del administrador principal',
|
||||||
@ -288,7 +288,7 @@ public function destroy($id)
|
|||||||
$user = User::findOrFail($id);
|
$user = User::findOrFail($id);
|
||||||
|
|
||||||
// Don't allow deleting admin
|
// Don't allow deleting admin
|
||||||
if ($user->email === 'marco@cnxifly.com') {
|
if ($user->email === 'marcoitaloesp@icloud.com') {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'No se puede eliminar el usuario administrador',
|
'message' => 'No se puede eliminar el usuario administrador',
|
||||||
|
|||||||
@ -8,19 +8,15 @@
|
|||||||
|
|
||||||
class AdminOnly
|
class AdminOnly
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Admin email - only this user can access restricted features
|
|
||||||
*/
|
|
||||||
private const ADMIN_EMAIL = 'marco@cnxifly.com';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
|
* Only users with is_admin = true can access admin routes.
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
if (!$user || $user->email !== self::ADMIN_EMAIL) {
|
if (!$user || !$user->is_admin) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Access denied. This feature is not available.',
|
'message' => 'Access denied. This feature is not available.',
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const Layout = ({ children }) => {
|
|||||||
const { date } = useFormatters();
|
const { date } = useFormatters();
|
||||||
|
|
||||||
// Admin email - only this user can see business module
|
// Admin email - only this user can see business module
|
||||||
const ADMIN_EMAIL = 'marco@cnxifly.com';
|
const ADMIN_EMAIL = 'marcoitaloesp@icloud.com';
|
||||||
const isAdmin = user?.email === ADMIN_EMAIL;
|
const isAdmin = user?.email === ADMIN_EMAIL;
|
||||||
|
|
||||||
// Mobile: sidebar oculta por padrão | Desktop: expandida
|
// Mobile: sidebar oculta por padrão | Desktop: expandida
|
||||||
|
|||||||
@ -568,6 +568,7 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"originalDescription": "Original Bank Description",
|
"originalDescription": "Original Bank Description",
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
|
"notesPlaceholder": "Add notes about this transaction (optional)",
|
||||||
"reference": "Reference",
|
"reference": "Reference",
|
||||||
"referencePlaceholder": "Document number, invoice, etc.",
|
"referencePlaceholder": "Document number, invoice, etc.",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
|||||||
@ -576,6 +576,7 @@
|
|||||||
"description": "Descripción",
|
"description": "Descripción",
|
||||||
"originalDescription": "Descripción Original del Banco",
|
"originalDescription": "Descripción Original del Banco",
|
||||||
"notes": "Observaciones",
|
"notes": "Observaciones",
|
||||||
|
"notesPlaceholder": "Añade observaciones sobre esta transacción (opcional)",
|
||||||
"reference": "Referencia",
|
"reference": "Referencia",
|
||||||
"referencePlaceholder": "Nº de documento, factura, etc.",
|
"referencePlaceholder": "Nº de documento, factura, etc.",
|
||||||
"date": "Fecha",
|
"date": "Fecha",
|
||||||
|
|||||||
@ -578,6 +578,7 @@
|
|||||||
"description": "Descrição",
|
"description": "Descrição",
|
||||||
"originalDescription": "Descrição Original do Banco",
|
"originalDescription": "Descrição Original do Banco",
|
||||||
"notes": "Observações",
|
"notes": "Observações",
|
||||||
|
"notesPlaceholder": "Adicione observações sobre esta transação (opcional)",
|
||||||
"reference": "Referência",
|
"reference": "Referência",
|
||||||
"referencePlaceholder": "Nº do documento, fatura, etc.",
|
"referencePlaceholder": "Nº do documento, fatura, etc.",
|
||||||
"date": "Data",
|
"date": "Data",
|
||||||
|
|||||||
@ -179,8 +179,18 @@ export default function Transactions() {
|
|||||||
category_id: '',
|
category_id: '',
|
||||||
cost_center_id: '',
|
cost_center_id: '',
|
||||||
add_keyword: true,
|
add_keyword: true,
|
||||||
|
notes: '',
|
||||||
});
|
});
|
||||||
const [savingQuickCategorize, setSavingQuickCategorize] = useState(false);
|
const [savingQuickCategorize, setSavingQuickCategorize] = useState(false);
|
||||||
|
// Estados para criar categoria/subcategoria inline no modal de categorização
|
||||||
|
const [showInlineCategoryForm, setShowInlineCategoryForm] = useState(false);
|
||||||
|
const [inlineCategoryData, setInlineCategoryData] = useState({
|
||||||
|
name: '',
|
||||||
|
parent_id: '',
|
||||||
|
type: 'expense',
|
||||||
|
icon: 'bi-tag',
|
||||||
|
});
|
||||||
|
const [savingInlineCategory, setSavingInlineCategory] = useState(false);
|
||||||
|
|
||||||
// Calcular se há filtros ativos (excluindo date_field que é sempre preenchido)
|
// Calcular se há filtros ativos (excluindo date_field que é sempre preenchido)
|
||||||
const hasActiveFilters = Object.entries(filters).some(([key, v]) => key !== 'date_field' && v !== '');
|
const hasActiveFilters = Object.entries(filters).some(([key, v]) => key !== 'date_field' && v !== '');
|
||||||
@ -615,10 +625,53 @@ export default function Transactions() {
|
|||||||
category_id: transaction.category_id || '',
|
category_id: transaction.category_id || '',
|
||||||
cost_center_id: transaction.cost_center_id || '',
|
cost_center_id: transaction.cost_center_id || '',
|
||||||
add_keyword: true,
|
add_keyword: true,
|
||||||
|
notes: transaction.notes || '',
|
||||||
|
});
|
||||||
|
setShowInlineCategoryForm(false);
|
||||||
|
setInlineCategoryData({
|
||||||
|
name: '',
|
||||||
|
parent_id: '',
|
||||||
|
type: transaction.type === 'credit' ? 'income' : 'expense',
|
||||||
|
icon: 'bi-tag',
|
||||||
});
|
});
|
||||||
setShowQuickCategorizeModal(true);
|
setShowQuickCategorizeModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Criar categoria/subcategoria inline no modal de categorização
|
||||||
|
const handleInlineCategorySubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!inlineCategoryData.name.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSavingInlineCategory(true);
|
||||||
|
const result = await categoryService.create({
|
||||||
|
...inlineCategoryData,
|
||||||
|
parent_id: inlineCategoryData.parent_id ? parseInt(inlineCategoryData.parent_id) : null,
|
||||||
|
});
|
||||||
|
const newCategory = result.data || result;
|
||||||
|
|
||||||
|
// Recarregar categorias
|
||||||
|
const categoriesRes = await categoryService.getAll({ flat: true });
|
||||||
|
setCategories(Array.isArray(categoriesRes) ? categoriesRes : (categoriesRes.data || []));
|
||||||
|
|
||||||
|
// Selecionar a nova categoria no modal
|
||||||
|
setQuickCategorizeData(prev => ({ ...prev, category_id: newCategory.id }));
|
||||||
|
|
||||||
|
showToast(t('categories.createSuccess'), 'success');
|
||||||
|
setShowInlineCategoryForm(false);
|
||||||
|
setInlineCategoryData({
|
||||||
|
name: '',
|
||||||
|
parent_id: '',
|
||||||
|
type: quickCategorizeData.transaction?.type === 'credit' ? 'income' : 'expense',
|
||||||
|
icon: 'bi-tag',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.response?.data?.message || t('categories.createError'), 'danger');
|
||||||
|
} finally {
|
||||||
|
setSavingInlineCategory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleQuickCategorizeSubmit = async (e) => {
|
const handleQuickCategorizeSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
@ -627,6 +680,7 @@ export default function Transactions() {
|
|||||||
const updateData = {
|
const updateData = {
|
||||||
category_id: quickCategorizeData.category_id || null,
|
category_id: quickCategorizeData.category_id || null,
|
||||||
cost_center_id: quickCategorizeData.cost_center_id || null,
|
cost_center_id: quickCategorizeData.cost_center_id || null,
|
||||||
|
notes: quickCategorizeData.notes || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Se add_keyword está ativo e há descrição original, criar keyword
|
// Se add_keyword está ativo e há descrição original, criar keyword
|
||||||
@ -638,8 +692,12 @@ export default function Transactions() {
|
|||||||
cost_center_id: updateData.cost_center_id,
|
cost_center_id: updateData.cost_center_id,
|
||||||
add_keyword: true,
|
add_keyword: true,
|
||||||
});
|
});
|
||||||
|
// Atualizar notas separadamente se houver
|
||||||
|
if (updateData.notes) {
|
||||||
|
await transactionService.update(quickCategorizeData.transaction.id, { notes: updateData.notes });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Apenas atualizar a transação
|
// Atualizar a transação com todos os dados
|
||||||
await transactionService.update(quickCategorizeData.transaction.id, updateData);
|
await transactionService.update(quickCategorizeData.transaction.id, updateData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1526,12 +1584,122 @@ export default function Transactions() {
|
|||||||
{transaction.type === 'credit' ? '+' : '-'}
|
{transaction.type === 'credit' ? '+' : '-'}
|
||||||
{formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)}
|
{formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="dropdown" onClick={(e) => e.stopPropagation()}>
|
||||||
className="btn btn-sm btn-outline-secondary"
|
<button className="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
onClick={() => openQuickCategorizeModal(transaction)}
|
<i className="bi bi-three-dots-vertical"></i>
|
||||||
>
|
</button>
|
||||||
<i className="bi bi-tags"></i>
|
<ul className="dropdown-menu dropdown-menu-end shadow-sm">
|
||||||
</button>
|
{/* Ações de Status */}
|
||||||
|
{transaction.status === 'pending' && (
|
||||||
|
<>
|
||||||
|
<li className="dropdown-header small text-muted">{t('transactions.status.label')}</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openCompleteModal(transaction)}>
|
||||||
|
<i className="bi bi-check-circle text-success me-2"></i>
|
||||||
|
{t('transactions.markComplete')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => handleQuickComplete(transaction)}>
|
||||||
|
<i className="bi bi-lightning-fill text-warning me-2"></i>
|
||||||
|
{t('transactions.quickComplete')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li><hr className="dropdown-divider" /></li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{transaction.status === 'completed' && (
|
||||||
|
<>
|
||||||
|
<li className="dropdown-header small text-muted">{t('transactions.status.label')}</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => handleRevert(transaction)}>
|
||||||
|
<i className="bi bi-arrow-counterclockwise text-warning me-2"></i>
|
||||||
|
{t('transactions.revert')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li><hr className="dropdown-divider" /></li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Ações Principais */}
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openQuickCategorizeModal(transaction)}>
|
||||||
|
<i className="bi bi-tags text-success me-2"></i>
|
||||||
|
{t('transactions.quickCategorize')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openEditModal(transaction)}>
|
||||||
|
<i className="bi bi-pencil text-primary me-2"></i>
|
||||||
|
{t('common.edit')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => handleDuplicate(transaction)}>
|
||||||
|
<i className="bi bi-copy text-info me-2"></i>
|
||||||
|
{t('transactions.duplicate')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{/* Dividir transação */}
|
||||||
|
{!transaction.is_transfer && !transaction.split_parent_id && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openSplitModal(transaction)}>
|
||||||
|
<i className="bi bi-diagram-3 text-secondary me-2"></i>
|
||||||
|
{t('transactions.split')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Converter em transferência */}
|
||||||
|
{!transaction.is_transfer && !transaction.is_split_child && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openConvertTransferModal(transaction)}>
|
||||||
|
<i className="bi bi-arrow-left-right text-purple me-2"></i>
|
||||||
|
{t('transactions.convertToTransfer')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Conciliar com passivo */}
|
||||||
|
{transaction.type === 'debit' && !transaction.is_reconciled && !transaction.is_transfer && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openReconcileLiabilityModal(transaction)}>
|
||||||
|
<i className="bi bi-link-45deg text-purple me-2"></i>
|
||||||
|
{t('transactions.reconcileWithLiability')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Criar Recorrência */}
|
||||||
|
{!transaction.is_transfer && !transaction.recurring_instance_id && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openRecurrenceModal(transaction)}>
|
||||||
|
<i className="bi bi-calendar-check text-info me-2"></i>
|
||||||
|
{t('recurring.makeRecurring')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cancelar */}
|
||||||
|
{transaction.status === 'pending' && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => handleCancel(transaction)}>
|
||||||
|
<i className="bi bi-x-circle text-secondary me-2"></i>
|
||||||
|
{t('transactions.cancel')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<li><hr className="dropdown-divider" /></li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item text-danger" onClick={() => handleDelete(transaction)}>
|
||||||
|
<i className="bi bi-trash me-2"></i>
|
||||||
|
{t('common.delete')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1631,13 +1799,122 @@ export default function Transactions() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center col-actions">
|
<td className="text-center col-actions">
|
||||||
<button
|
<div className="dropdown">
|
||||||
className="txn-actions-btn"
|
<button className="txn-actions-btn" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
onClick={() => openQuickCategorizeModal(transaction)}
|
<i className="bi bi-three-dots-vertical"></i>
|
||||||
title={t('transactions.quickCategorize')}
|
</button>
|
||||||
>
|
<ul className="dropdown-menu dropdown-menu-end shadow-sm">
|
||||||
<i className="bi bi-tags"></i>
|
{/* Ações de Status */}
|
||||||
</button>
|
{transaction.status === 'pending' && (
|
||||||
|
<>
|
||||||
|
<li className="dropdown-header small text-muted">{t('transactions.status.label')}</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openCompleteModal(transaction)}>
|
||||||
|
<i className="bi bi-check-circle text-success me-2"></i>
|
||||||
|
{t('transactions.markComplete')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => handleQuickComplete(transaction)}>
|
||||||
|
<i className="bi bi-lightning-fill text-warning me-2"></i>
|
||||||
|
{t('transactions.quickComplete')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li><hr className="dropdown-divider" /></li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{transaction.status === 'completed' && (
|
||||||
|
<>
|
||||||
|
<li className="dropdown-header small text-muted">{t('transactions.status.label')}</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => handleRevert(transaction)}>
|
||||||
|
<i className="bi bi-arrow-counterclockwise text-warning me-2"></i>
|
||||||
|
{t('transactions.revert')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li><hr className="dropdown-divider" /></li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Ações Principais */}
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openQuickCategorizeModal(transaction)}>
|
||||||
|
<i className="bi bi-tags text-success me-2"></i>
|
||||||
|
{t('transactions.quickCategorize')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openEditModal(transaction)}>
|
||||||
|
<i className="bi bi-pencil text-primary me-2"></i>
|
||||||
|
{t('common.edit')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => handleDuplicate(transaction)}>
|
||||||
|
<i className="bi bi-copy text-info me-2"></i>
|
||||||
|
{t('transactions.duplicate')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{/* Dividir transação */}
|
||||||
|
{!transaction.is_transfer && !transaction.split_parent_id && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openSplitModal(transaction)}>
|
||||||
|
<i className="bi bi-diagram-3 text-secondary me-2"></i>
|
||||||
|
{t('transactions.split')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Converter em transferência */}
|
||||||
|
{!transaction.is_transfer && !transaction.is_split_child && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openConvertTransferModal(transaction)}>
|
||||||
|
<i className="bi bi-arrow-left-right text-purple me-2"></i>
|
||||||
|
{t('transactions.convertToTransfer')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Conciliar com passivo */}
|
||||||
|
{transaction.type === 'debit' && !transaction.is_reconciled && !transaction.is_transfer && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openReconcileLiabilityModal(transaction)}>
|
||||||
|
<i className="bi bi-link-45deg text-purple me-2"></i>
|
||||||
|
{t('transactions.reconcileWithLiability')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Criar Recorrência */}
|
||||||
|
{!transaction.is_transfer && !transaction.recurring_instance_id && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => openRecurrenceModal(transaction)}>
|
||||||
|
<i className="bi bi-calendar-check text-info me-2"></i>
|
||||||
|
{t('recurring.makeRecurring')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cancelar */}
|
||||||
|
{transaction.status === 'pending' && (
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item" onClick={() => handleCancel(transaction)}>
|
||||||
|
<i className="bi bi-x-circle text-secondary me-2"></i>
|
||||||
|
{t('transactions.cancel')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<li><hr className="dropdown-divider" /></li>
|
||||||
|
<li>
|
||||||
|
<button className="dropdown-item text-danger" onClick={() => handleDelete(transaction)}>
|
||||||
|
<i className="bi bi-trash me-2"></i>
|
||||||
|
{t('common.delete')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@ -3810,7 +4087,10 @@ export default function Transactions() {
|
|||||||
{/* Modal de Categorização Rápida Individual */}
|
{/* Modal de Categorização Rápida Individual */}
|
||||||
<Modal
|
<Modal
|
||||||
show={showQuickCategorizeModal}
|
show={showQuickCategorizeModal}
|
||||||
onClose={() => setShowQuickCategorizeModal(false)}
|
onClose={() => {
|
||||||
|
setShowQuickCategorizeModal(false);
|
||||||
|
setShowInlineCategoryForm(false);
|
||||||
|
}}
|
||||||
title={t('transactions.quickCategorize')}
|
title={t('transactions.quickCategorize')}
|
||||||
size="md"
|
size="md"
|
||||||
>
|
>
|
||||||
@ -3836,14 +4116,81 @@ export default function Transactions() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Seção Categoria com botão de criar */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label">{t('transactions.category')}</label>
|
<div className="d-flex justify-content-between align-items-center mb-1">
|
||||||
<CategorySelector
|
<label className="form-label mb-0">{t('transactions.category')}</label>
|
||||||
categories={categories}
|
<button
|
||||||
value={quickCategorizeData.category_id}
|
type="button"
|
||||||
onChange={(e) => setQuickCategorizeData(prev => ({ ...prev, category_id: e.target.value }))}
|
className="btn btn-link btn-sm p-0 text-decoration-none"
|
||||||
placeholder={t('transactions.selectCategory')}
|
onClick={() => setShowInlineCategoryForm(!showInlineCategoryForm)}
|
||||||
/>
|
>
|
||||||
|
<i className={`bi ${showInlineCategoryForm ? 'bi-x-circle' : 'bi-plus-circle'} me-1`}></i>
|
||||||
|
{showInlineCategoryForm ? t('common.cancel') : t('categories.newCategory')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showInlineCategoryForm ? (
|
||||||
|
<div className="card border-primary mb-2" style={{ backgroundColor: '#0f172a' }}>
|
||||||
|
<div className="card-body p-3">
|
||||||
|
<div className="mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={inlineCategoryData.name}
|
||||||
|
onChange={(e) => setInlineCategoryData(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
placeholder={t('categories.categoryName')}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="row g-2 mb-2">
|
||||||
|
<div className="col-6">
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm"
|
||||||
|
value={inlineCategoryData.parent_id}
|
||||||
|
onChange={(e) => setInlineCategoryData(prev => ({ ...prev, parent_id: e.target.value }))}
|
||||||
|
>
|
||||||
|
<option value="">{t('categories.noParent')}</option>
|
||||||
|
{parentCategories.map(cat => (
|
||||||
|
<option key={cat.id} value={cat.id}>{cat.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="col-6">
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm"
|
||||||
|
value={inlineCategoryData.type}
|
||||||
|
onChange={(e) => setInlineCategoryData(prev => ({ ...prev, type: e.target.value }))}
|
||||||
|
>
|
||||||
|
<option value="expense">{t('categories.types.expense')}</option>
|
||||||
|
<option value="income">{t('categories.types.income')}</option>
|
||||||
|
<option value="both">{t('categories.types.both')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small className="text-muted d-block mb-2">{t('transactions.quickCategory.parentHelp')}</small>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm w-100"
|
||||||
|
onClick={handleInlineCategorySubmit}
|
||||||
|
disabled={savingInlineCategory || !inlineCategoryData.name.trim()}
|
||||||
|
>
|
||||||
|
{savingInlineCategory ? (
|
||||||
|
<><span className="spinner-border spinner-border-sm me-1"></span>{t('common.saving')}</>
|
||||||
|
) : (
|
||||||
|
<><i className="bi bi-plus-lg me-1"></i>{inlineCategoryData.parent_id ? t('categories.createSubcategory') : t('categories.newCategory')}</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CategorySelector
|
||||||
|
categories={categories}
|
||||||
|
value={quickCategorizeData.category_id}
|
||||||
|
onChange={(e) => setQuickCategorizeData(prev => ({ ...prev, category_id: e.target.value }))}
|
||||||
|
placeholder={t('transactions.selectCategory')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
@ -3860,6 +4207,18 @@ export default function Transactions() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Campo de Observações */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="form-label">{t('transactions.notes')}</label>
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
rows="2"
|
||||||
|
value={quickCategorizeData.notes}
|
||||||
|
onChange={(e) => setQuickCategorizeData(prev => ({ ...prev, notes: e.target.value }))}
|
||||||
|
placeholder={t('transactions.notesPlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{quickCategorizeData.transaction?.original_description && (
|
{quickCategorizeData.transaction?.original_description && (
|
||||||
<div className="form-check mb-3">
|
<div className="form-check mb-3">
|
||||||
<input
|
<input
|
||||||
@ -3877,7 +4236,10 @@ export default function Transactions() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="d-flex justify-content-end gap-2">
|
<div className="d-flex justify-content-end gap-2">
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => setShowQuickCategorizeModal(false)}>
|
<button type="button" className="btn btn-secondary" onClick={() => {
|
||||||
|
setShowQuickCategorizeModal(false);
|
||||||
|
setShowInlineCategoryForm(false);
|
||||||
|
}}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn btn-success" disabled={savingQuickCategorize}>
|
<button type="submit" className="btn btn-success" disabled={savingQuickCategorize}>
|
||||||
|
|||||||
@ -515,7 +515,7 @@ function Users() {
|
|||||||
setSelectedUser(user);
|
setSelectedUser(user);
|
||||||
setShowDeleteModal(true);
|
setShowDeleteModal(true);
|
||||||
}}
|
}}
|
||||||
disabled={user.email === 'marco@cnxifly.com'}
|
disabled={user.email === 'marcoitaloesp@icloud.com'}
|
||||||
className="btn btn-sm btn-outline-danger"
|
className="btn btn-sm btn-outline-danger"
|
||||||
title="Eliminar usuario"
|
title="Eliminar usuario"
|
||||||
>
|
>
|
||||||
@ -1117,7 +1117,7 @@ function Users() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Admin Toggle - Only show if not the main admin */}
|
{/* Admin Toggle - Only show if not the main admin */}
|
||||||
{editingUser.email !== 'marco@cnxifly.com' && (
|
{editingUser.email !== 'marcoitaloesp@icloud.com' && (
|
||||||
<div className="form-check form-switch mb-3">
|
<div className="form-check form-switch mb-3">
|
||||||
<input
|
<input
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user