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)} - +