From 99a68f4520971bf5cb4dc0db80805eb520b4efd7 Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Dec 2025 21:15:36 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20novas=20categorias=20e=20melhorias=20UI?= =?UTF-8?q?=20transa=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- .../Api/UserManagementController.php | 4 +- backend/app/Http/Middleware/AdminOnly.php | 8 +- frontend/src/components/Layout.jsx | 2 +- frontend/src/i18n/locales/en.json | 1 + frontend/src/i18n/locales/es.json | 1 + frontend/src/i18n/locales/pt-BR.json | 1 + frontend/src/pages/TransactionsByWeek.jsx | 408 +++++++++++++++++- frontend/src/pages/Users.jsx | 4 +- 8 files changed, 395 insertions(+), 34 deletions(-) diff --git a/backend/app/Http/Controllers/Api/UserManagementController.php b/backend/app/Http/Controllers/Api/UserManagementController.php index 1e1e46e..bb7ae7b 100755 --- a/backend/app/Http/Controllers/Api/UserManagementController.php +++ b/backend/app/Http/Controllers/Api/UserManagementController.php @@ -228,7 +228,7 @@ public function update(Request $request, $id) $user = User::findOrFail($id); // 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([ 'success' => false, 'message' => 'No se puede remover permisos del administrador principal', @@ -288,7 +288,7 @@ public function destroy($id) $user = User::findOrFail($id); // Don't allow deleting admin - if ($user->email === 'marco@cnxifly.com') { + if ($user->email === 'marcoitaloesp@icloud.com') { return response()->json([ 'success' => false, 'message' => 'No se puede eliminar el usuario administrador', diff --git a/backend/app/Http/Middleware/AdminOnly.php b/backend/app/Http/Middleware/AdminOnly.php index fb5f5c5..152c314 100755 --- a/backend/app/Http/Middleware/AdminOnly.php +++ b/backend/app/Http/Middleware/AdminOnly.php @@ -8,19 +8,15 @@ class AdminOnly { - /** - * Admin email - only this user can access restricted features - */ - private const ADMIN_EMAIL = 'marco@cnxifly.com'; - /** * Handle an incoming request. + * Only users with is_admin = true can access admin routes. */ public function handle(Request $request, Closure $next): Response { $user = $request->user(); - if (!$user || $user->email !== self::ADMIN_EMAIL) { + if (!$user || !$user->is_admin) { return response()->json([ 'success' => false, 'message' => 'Access denied. This feature is not available.', diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 8d57ada..ecd52e0 100755 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -14,7 +14,7 @@ const Layout = ({ children }) => { const { date } = useFormatters(); // 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; // Mobile: sidebar oculta por padrão | Desktop: expandida diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index de3662f..683117a 100755 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -568,6 +568,7 @@ "description": "Description", "originalDescription": "Original Bank Description", "notes": "Notes", + "notesPlaceholder": "Add notes about this transaction (optional)", "reference": "Reference", "referencePlaceholder": "Document number, invoice, etc.", "date": "Date", diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index d7f7a16..e588225 100755 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -576,6 +576,7 @@ "description": "Descripción", "originalDescription": "Descripción Original del Banco", "notes": "Observaciones", + "notesPlaceholder": "Añade observaciones sobre esta transacción (opcional)", "reference": "Referencia", "referencePlaceholder": "Nº de documento, factura, etc.", "date": "Fecha", diff --git a/frontend/src/i18n/locales/pt-BR.json b/frontend/src/i18n/locales/pt-BR.json index 22858de..86eaa40 100755 --- a/frontend/src/i18n/locales/pt-BR.json +++ b/frontend/src/i18n/locales/pt-BR.json @@ -578,6 +578,7 @@ "description": "Descrição", "originalDescription": "Descrição Original do Banco", "notes": "Observações", + "notesPlaceholder": "Adicione observações sobre esta transação (opcional)", "reference": "Referência", "referencePlaceholder": "Nº do documento, fatura, etc.", "date": "Data", diff --git a/frontend/src/pages/TransactionsByWeek.jsx b/frontend/src/pages/TransactionsByWeek.jsx index 7ba5ef5..aecd995 100755 --- a/frontend/src/pages/TransactionsByWeek.jsx +++ b/frontend/src/pages/TransactionsByWeek.jsx @@ -179,8 +179,18 @@ export default function Transactions() { category_id: '', cost_center_id: '', add_keyword: true, + notes: '', }); 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) 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 || '', cost_center_id: transaction.cost_center_id || '', add_keyword: true, + notes: transaction.notes || '', + }); + setShowInlineCategoryForm(false); + setInlineCategoryData({ + name: '', + parent_id: '', + type: transaction.type === 'credit' ? 'income' : 'expense', + icon: 'bi-tag', }); 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) => { e.preventDefault(); try { @@ -627,6 +680,7 @@ export default function Transactions() { const updateData = { category_id: quickCategorizeData.category_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 @@ -638,8 +692,12 @@ export default function Transactions() { cost_center_id: updateData.cost_center_id, add_keyword: true, }); + // Atualizar notas separadamente se houver + if (updateData.notes) { + await transactionService.update(quickCategorizeData.transaction.id, { notes: updateData.notes }); + } } else { - // Apenas atualizar a transação + // Atualizar a transação com todos os dados await transactionService.update(quickCategorizeData.transaction.id, updateData); } @@ -1526,12 +1584,122 @@ export default function Transactions() { {transaction.type === 'credit' ? '+' : '-'} {formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)} - +
e.stopPropagation()}> + + +
@@ -1631,13 +1799,122 @@ export default function Transactions() { - +
+ + +
))} @@ -3810,7 +4087,10 @@ export default function Transactions() { {/* Modal de Categorização Rápida Individual */} setShowQuickCategorizeModal(false)} + onClose={() => { + setShowQuickCategorizeModal(false); + setShowInlineCategoryForm(false); + }} title={t('transactions.quickCategorize')} size="md" > @@ -3836,14 +4116,81 @@ export default function Transactions() { )} + {/* Seção Categoria com botão de criar */}
- - setQuickCategorizeData(prev => ({ ...prev, category_id: e.target.value }))} - placeholder={t('transactions.selectCategory')} - /> +
+ + +
+ + {showInlineCategoryForm ? ( +
+
+
+ setInlineCategoryData(prev => ({ ...prev, name: e.target.value }))} + placeholder={t('categories.categoryName')} + autoFocus + /> +
+
+
+ +
+
+ +
+
+ {t('transactions.quickCategory.parentHelp')} + +
+
+ ) : ( + setQuickCategorizeData(prev => ({ ...prev, category_id: e.target.value }))} + placeholder={t('transactions.selectCategory')} + /> + )}
@@ -3860,6 +4207,18 @@ export default function Transactions() {
+ {/* Campo de Observações */} +
+ +