Add category edit action
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user