Add category edit action

This commit is contained in:
Cauê Faleiros
2026-08-10 11:15:15 -03:00
parent 4556659fcf
commit e85dbe6f10
4 changed files with 94 additions and 11 deletions

View File

@@ -10,7 +10,8 @@ const {
getCatalogSummary, getCatalogSummary,
listCategories, listCategories,
listConsumptionReferences, listConsumptionReferences,
listProducts listProducts,
updateCategory
} = require('../services/catalogService'); } = require('../services/catalogService');
const { importTinyProductCompositionExport } = require('../services/productionOrderService'); const { importTinyProductCompositionExport } = require('../services/productionOrderService');
@@ -40,6 +41,14 @@ router.post('/catalog/categories', verifyToken, async (req, res, next) => {
} }
}); });
router.put('/catalog/categories/:id', verifyToken, async (req, res, next) => {
try {
res.json(await updateCategory(req.params.id, req.body || {}));
} catch (error) {
next(error);
}
});
router.delete('/catalog/categories/:id', verifyToken, async (req, res, next) => { router.delete('/catalog/categories/:id', verifyToken, async (req, res, next) => {
try { try {
await deleteCategory(req.params.id); await deleteCategory(req.params.id);

View File

@@ -110,6 +110,33 @@ const createCategory = async ({ name, description }) => {
return mapCategory(result.rows[0]); return mapCategory(result.rows[0]);
}; };
const updateCategory = async (id, { name, description }) => {
const normalizedId = Number(id);
const normalizedName = normalizeText(name);
if (!normalizedId || !normalizedName) {
const error = new Error('Nome da categoria é obrigatório.');
error.statusCode = 400;
throw error;
}
const result = await pool.query(`
UPDATE catalog_categories
SET name = $1,
description = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $3
RETURNING id, name, description, created_at, updated_at
`, [normalizedName, normalizeText(description) || null, normalizedId]);
if (!result.rows.length) {
const error = new Error('Categoria não encontrada.');
error.statusCode = 404;
throw error;
}
return mapCategory(result.rows[0]);
};
const deleteCategory = async (id) => { const deleteCategory = async (id) => {
await pool.query('DELETE FROM catalog_categories WHERE id = $1', [id]); await pool.query('DELETE FROM catalog_categories WHERE id = $1', [id]);
}; };
@@ -272,5 +299,6 @@ module.exports = {
getCatalogSummary, getCatalogSummary,
listCategories, listCategories,
listConsumptionReferences, listConsumptionReferences,
listProducts listProducts,
updateCategory
}; };

View File

@@ -291,6 +291,21 @@ export const saveCatalogCategory = async (payload: CatalogCategoryPayload): Prom
return data as CatalogCategory; return data as CatalogCategory;
}; };
export const updateCatalogCategory = async (id: number, payload: CatalogCategoryPayload): Promise<CatalogCategory> => {
const response = await authFetch(`/catalog/categories/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(data?.error || 'Não foi possível atualizar a categoria.');
}
return data as CatalogCategory;
};
export const deleteCatalogCategory = async (id: number): Promise<void> => { export const deleteCatalogCategory = async (id: number): Promise<void> => {
const response = await authFetch(`/catalog/categories/${id}`, { method: 'DELETE' }); const response = await authFetch(`/catalog/categories/${id}`, { method: 'DELETE' });
if (!response.ok) { if (!response.ok) {

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { ClipboardList, Loader2, Package, RefreshCw, Ruler, Save, Tags, Trash2, Upload } from 'lucide-react'; import { ClipboardList, Loader2, Package, Pencil, RefreshCw, Ruler, Save, Tags, Trash2, Upload } from 'lucide-react';
import { import {
deleteCatalogCategory, deleteCatalogCategory,
deleteCatalogProduct, deleteCatalogProduct,
@@ -9,7 +9,8 @@ import {
importProductCompositions, importProductCompositions,
saveCatalogCategory, saveCatalogCategory,
saveCatalogProduct, saveCatalogProduct,
saveConsumptionReference saveConsumptionReference,
updateCatalogCategory
} from '../dataService'; } from '../dataService';
import PaginationControls from '../components/PaginationControls'; import PaginationControls from '../components/PaginationControls';
import type { CatalogCategory, CatalogProduct, CatalogProductType, CatalogSummary, ConsumptionReference } from '../types'; import type { CatalogCategory, CatalogProduct, CatalogProductType, CatalogSummary, ConsumptionReference } from '../types';
@@ -124,6 +125,7 @@ const Registrations = () => {
const compositionFileInputRef = useRef<HTMLInputElement>(null); const compositionFileInputRef = useRef<HTMLInputElement>(null);
const [categoryForm, setCategoryForm] = useState({ name: '', description: '' }); const [categoryForm, setCategoryForm] = useState({ name: '', description: '' });
const [editingCategory, setEditingCategory] = useState<CatalogCategory | null>(null);
const [productForm, setProductForm] = useState(defaultProductForm); const [productForm, setProductForm] = useState(defaultProductForm);
const [referenceForm, setReferenceForm] = useState({ const [referenceForm, setReferenceForm] = useState({
productId: '', productId: '',
@@ -291,9 +293,14 @@ const Registrations = () => {
}; };
const saveCategory = () => runAction(async () => { const saveCategory = () => runAction(async () => {
await saveCatalogCategory(categoryForm); if (editingCategory) {
await updateCatalogCategory(editingCategory.id, categoryForm);
} else {
await saveCatalogCategory(categoryForm);
}
setCategoryForm({ name: '', description: '' }); setCategoryForm({ name: '', description: '' });
}, 'Categoria salva.'); setEditingCategory(null);
}, editingCategory ? 'Categoria atualizada.' : 'Categoria salva.');
const saveProduct = () => runAction(async () => { const saveProduct = () => runAction(async () => {
await saveCatalogProduct({ await saveCatalogProduct({
@@ -386,6 +393,18 @@ const Registrations = () => {
`Categoria ${category.name} excluida.` `Categoria ${category.name} excluida.`
); );
const editCategory = (category: CatalogCategory) => {
setEditingCategory(category);
setCategoryForm({ name: category.name, description: category.description || '' });
setStatus('idle');
setFeedback('');
};
const cancelCategoryEdit = () => {
setEditingCategory(null);
setCategoryForm({ name: '', description: '' });
};
const removeProduct = (product: CatalogProduct) => runAction( const removeProduct = (product: CatalogProduct) => runAction(
() => deleteCatalogProduct(product.id), () => deleteCatalogProduct(product.id),
`Produto ${product.sku} excluido.` `Produto ${product.sku} excluido.`
@@ -713,9 +732,14 @@ const Registrations = () => {
<h3 className="text-sm font-bold text-dark-text">{category.name}</h3> <h3 className="text-sm font-bold text-dark-text">{category.name}</h3>
<p className="mt-1 text-xs font-semibold text-dark-muted">{category.description || 'Sem descricao'}</p> <p className="mt-1 text-xs font-semibold text-dark-muted">{category.description || 'Sem descricao'}</p>
</div> </div>
<button type="button" onClick={() => void removeCategory(category)} className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-red-400/40 hover:text-red-300 cursor-pointer"> <div className="flex shrink-0 items-center gap-2">
<Trash2 className="h-4 w-4" /> <button type="button" onClick={() => editCategory(category)} aria-label={`Editar categoria ${category.name}`} title="Editar categoria" className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-brand-primary/50 hover:text-brand-primary cursor-pointer">
</button> <Pencil className="h-4 w-4" />
</button>
<button type="button" onClick={() => void removeCategory(category)} aria-label={`Excluir categoria ${category.name}`} title="Excluir categoria" className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-dark-border bg-dark-input text-dark-muted transition-colors hover:border-red-400/40 hover:text-red-300 cursor-pointer">
<Trash2 className="h-4 w-4" />
</button>
</div>
</div> </div>
))} ))}
{!catalog.categories.length && <div className={emptyStateClassName}>Nenhuma categoria cadastrada.</div>} {!catalog.categories.length && <div className={emptyStateClassName}>Nenhuma categoria cadastrada.</div>}
@@ -736,7 +760,14 @@ const Registrations = () => {
</div> </div>
<div className={formPanelClassName}> <div className={formPanelClassName}>
<h2 className="text-sm font-bold text-dark-text">Nova categoria</h2> <div className="flex items-center justify-between gap-3">
<h2 className="text-sm font-bold text-dark-text">{editingCategory ? 'Editar categoria' : 'Nova categoria'}</h2>
{editingCategory && (
<button type="button" onClick={cancelCategoryEdit} className="text-xs font-bold text-dark-muted transition-colors hover:text-dark-text cursor-pointer">
Cancelar
</button>
)}
</div>
<div className="mt-4 space-y-3"> <div className="mt-4 space-y-3">
<label className={`block ${labelClassName}`}> <label className={`block ${labelClassName}`}>
Nome Nome
@@ -748,7 +779,7 @@ const Registrations = () => {
</label> </label>
<button type="button" onClick={() => void saveCategory()} disabled={status === 'saving'} className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-xl bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-colors hover:bg-brand-primary/90 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"> <button type="button" onClick={() => void saveCategory()} disabled={status === 'saving'} className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-xl bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-colors hover:bg-brand-primary/90 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer">
<Save className="h-4 w-4" /> <Save className="h-4 w-4" />
Salvar categoria {editingCategory ? 'Salvar alterações' : 'Salvar categoria'}
</button> </button>
</div> </div>
</div> </div>