Add category edit action
This commit is contained in:
@@ -10,7 +10,8 @@ const {
|
||||
getCatalogSummary,
|
||||
listCategories,
|
||||
listConsumptionReferences,
|
||||
listProducts
|
||||
listProducts,
|
||||
updateCategory
|
||||
} = require('../services/catalogService');
|
||||
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) => {
|
||||
try {
|
||||
await deleteCategory(req.params.id);
|
||||
|
||||
@@ -110,6 +110,33 @@ const createCategory = async ({ name, description }) => {
|
||||
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) => {
|
||||
await pool.query('DELETE FROM catalog_categories WHERE id = $1', [id]);
|
||||
};
|
||||
@@ -272,5 +299,6 @@ module.exports = {
|
||||
getCatalogSummary,
|
||||
listCategories,
|
||||
listConsumptionReferences,
|
||||
listProducts
|
||||
listProducts,
|
||||
updateCategory
|
||||
};
|
||||
|
||||
@@ -291,6 +291,21 @@ export const saveCatalogCategory = async (payload: CatalogCategoryPayload): Prom
|
||||
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> => {
|
||||
const response = await authFetch(`/catalog/categories/${id}`, { method: 'DELETE' });
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
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 {
|
||||
deleteCatalogCategory,
|
||||
deleteCatalogProduct,
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
importProductCompositions,
|
||||
saveCatalogCategory,
|
||||
saveCatalogProduct,
|
||||
saveConsumptionReference
|
||||
saveConsumptionReference,
|
||||
updateCatalogCategory
|
||||
} from '../dataService';
|
||||
import PaginationControls from '../components/PaginationControls';
|
||||
import type { CatalogCategory, CatalogProduct, CatalogProductType, CatalogSummary, ConsumptionReference } from '../types';
|
||||
@@ -124,6 +125,7 @@ const Registrations = () => {
|
||||
const compositionFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [categoryForm, setCategoryForm] = useState({ name: '', description: '' });
|
||||
const [editingCategory, setEditingCategory] = useState<CatalogCategory | null>(null);
|
||||
const [productForm, setProductForm] = useState(defaultProductForm);
|
||||
const [referenceForm, setReferenceForm] = useState({
|
||||
productId: '',
|
||||
@@ -291,9 +293,14 @@ const Registrations = () => {
|
||||
};
|
||||
|
||||
const saveCategory = () => runAction(async () => {
|
||||
await saveCatalogCategory(categoryForm);
|
||||
if (editingCategory) {
|
||||
await updateCatalogCategory(editingCategory.id, categoryForm);
|
||||
} else {
|
||||
await saveCatalogCategory(categoryForm);
|
||||
}
|
||||
setCategoryForm({ name: '', description: '' });
|
||||
}, 'Categoria salva.');
|
||||
setEditingCategory(null);
|
||||
}, editingCategory ? 'Categoria atualizada.' : 'Categoria salva.');
|
||||
|
||||
const saveProduct = () => runAction(async () => {
|
||||
await saveCatalogProduct({
|
||||
@@ -386,6 +393,18 @@ const Registrations = () => {
|
||||
`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(
|
||||
() => deleteCatalogProduct(product.id),
|
||||
`Produto ${product.sku} excluido.`
|
||||
@@ -713,9 +732,14 @@ const Registrations = () => {
|
||||
<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>
|
||||
</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">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<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">
|
||||
<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>
|
||||
))}
|
||||
{!catalog.categories.length && <div className={emptyStateClassName}>Nenhuma categoria cadastrada.</div>}
|
||||
@@ -736,7 +760,14 @@ const Registrations = () => {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<label className={`block ${labelClassName}`}>
|
||||
Nome
|
||||
@@ -748,7 +779,7 @@ const Registrations = () => {
|
||||
</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">
|
||||
<Save className="h-4 w-4" />
|
||||
Salvar categoria
|
||||
{editingCategory ? 'Salvar alterações' : 'Salvar categoria'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user