feat: tags fixes
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
// src/api/CategoriesApi.js
|
// src/api/CategoriesApi.js
|
||||||
export default class CategoriesApi {
|
export default class CategoriesApi {
|
||||||
constructor(token) {
|
constructor(token) {
|
||||||
this.baseUrl = 'https://inventory-bff.dream-views.com/api/v1/Tags';
|
this.tagUrl = 'https://inventory-bff.dream-views.com/api/v1/Tag'; // <— singular
|
||||||
|
this.tagTypeUrl = 'https://inventory-bff.dream-views.com/api/v1/TagType';
|
||||||
this.token = token;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,8 +14,9 @@ export default class CategoriesApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TAGS
|
||||||
async getAll() {
|
async getAll() {
|
||||||
const res = await fetch(`${this.baseUrl}/GetAll`, {
|
const res = await fetch(`${this.tagUrl}/GetAll`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this.headers(false),
|
headers: this.headers(false),
|
||||||
});
|
});
|
||||||
@@ -22,8 +24,8 @@ export default class CategoriesApi {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(payload) {
|
async create(payload) { // CreateTagRequest
|
||||||
const res = await fetch(`${this.baseUrl}/Create`, {
|
const res = await fetch(`${this.tagUrl}/Create`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.headers(),
|
headers: this.headers(),
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -32,8 +34,8 @@ export default class CategoriesApi {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(payload) {
|
async update(payload) { // UpdateTagRequest
|
||||||
const res = await fetch(`${this.baseUrl}/Update`, {
|
const res = await fetch(`${this.tagUrl}/Update`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: this.headers(),
|
headers: this.headers(),
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -42,8 +44,18 @@ export default class CategoriesApi {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeStatus(payload) { // { id, status }
|
||||||
|
const res = await fetch(`${this.tagUrl}/ChangeStatus`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: this.headers(),
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`ChangeStatus error ${res.status}: ${await res.text()}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
async delete(payload) {
|
async delete(payload) {
|
||||||
const res = await fetch(`${this.baseUrl}/Delete`, {
|
const res = await fetch(`${this.tagUrl}/Delete`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: this.headers(),
|
headers: this.headers(),
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -51,4 +63,14 @@ export default class CategoriesApi {
|
|||||||
if (!res.ok) throw new Error(`Delete error ${res.status}: ${await res.text()}`);
|
if (!res.ok) throw new Error(`Delete error ${res.status}: ${await res.text()}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TAG TYPES (para <select> de typeId)
|
||||||
|
async getAllTypes() {
|
||||||
|
const res = await fetch(`${this.tagTypeUrl}/GetAll`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.headers(false),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`GetAllTypes error ${res.status}: ${await res.text()}`);
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,107 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Box, Button, Paper, TextField, Typography } from '@mui/material';
|
import {
|
||||||
|
Box, Button, Paper, TextField, Typography,
|
||||||
|
FormControl, InputLabel, Select, MenuItem, Chip, OutlinedInput
|
||||||
|
} from '@mui/material';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import CategoriesApi from '../../api/CategoriesApi';
|
import CategoriesApi from '../../api/CategoriesApi';
|
||||||
|
|
||||||
|
const toSlug = (s) =>
|
||||||
|
(s ?? '').toString().trim().toLowerCase()
|
||||||
|
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)+/g, '');
|
||||||
|
|
||||||
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) {
|
export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel }) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
const token = user?.thalosToken || localStorage.getItem('thalosToken');
|
||||||
const api = useMemo(() => new CategoriesApi(token), [token]);
|
const api = useMemo(() => new CategoriesApi(token), [token]);
|
||||||
|
|
||||||
|
const [types, setTypes] = useState([]);
|
||||||
|
const [allTags, setAllTags] = useState([]);
|
||||||
|
|
||||||
const [category, setCategory] = useState({
|
const [category, setCategory] = useState({
|
||||||
_Id: '',
|
_Id: '',
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
slug: '',
|
||||||
|
typeId: '',
|
||||||
|
parentTagId: [], // array<string>
|
||||||
|
displayOrder: 0,
|
||||||
|
icon: '',
|
||||||
status: 'Active',
|
status: 'Active',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// cargar Tag Types y Tags para selects
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const [t, tags] = await Promise.all([
|
||||||
|
api.getAllTypes(),
|
||||||
|
api.getAll(),
|
||||||
|
]);
|
||||||
|
setTypes(Array.isArray(t) ? t : []);
|
||||||
|
setAllTags(Array.isArray(tags) ? tags : []);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Loading form dictionaries failed:', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
setCategory({
|
const _Id = initialData._id || initialData._Id || '';
|
||||||
_Id: initialData._id || initialData._Id || '',
|
const id = initialData.id || initialData.Id || _Id || '';
|
||||||
id: initialData.id || initialData.Id || initialData._id || initialData._Id || '',
|
const name = initialData.tagName ?? initialData.name ?? '';
|
||||||
name: initialData.name ?? '',
|
const slug = initialData.slug ?? toSlug(name);
|
||||||
description: initialData.description ?? '',
|
const status = initialData.status ?? 'Active';
|
||||||
status: initialData.status ?? 'Active',
|
const typeId = initialData.typeId ?? '';
|
||||||
});
|
const parentTagId = Array.isArray(initialData.parentTagId) ? initialData.parentTagId : [];
|
||||||
|
const displayOrder = Number.isFinite(initialData.displayOrder) ? initialData.displayOrder : 0;
|
||||||
|
const icon = initialData.icon ?? '';
|
||||||
|
setCategory({ _Id, id, name, slug, status, typeId, parentTagId, displayOrder, icon });
|
||||||
} else {
|
} else {
|
||||||
setCategory({ _Id: '', id: '', name: '', description: '', status: 'Active' });
|
setCategory({
|
||||||
|
_Id: '', id: '', name: '', slug: '', status: 'Active',
|
||||||
|
typeId: '', parentTagId: [], displayOrder: 0, icon: ''
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const updateField = (name, value) => {
|
||||||
const { name, value } = e.target;
|
|
||||||
setCategory((prev) => ({ ...prev, [name]: value }));
|
setCategory((prev) => ({ ...prev, [name]: value }));
|
||||||
|
if (name === 'name' && !category._Id) {
|
||||||
|
// autogenera slug en "create"
|
||||||
|
const maybe = toSlug(value);
|
||||||
|
setCategory((prev) => ({ ...prev, slug: maybe }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
if (category._Id) {
|
if (category._Id) {
|
||||||
|
// UpdateTagRequest
|
||||||
const payload = {
|
const payload = {
|
||||||
_Id: category._Id,
|
id: category.id || category._Id,
|
||||||
Id: category.id || category._Id,
|
tenantId: user?.tenantId ?? undefined,
|
||||||
name: category.name,
|
tagName: category.name,
|
||||||
description: category.description,
|
typeId: category.typeId || null,
|
||||||
status: category.status,
|
parentTagId: category.parentTagId?.length ? category.parentTagId : [],
|
||||||
|
slug: category.slug,
|
||||||
|
displayOrder: Number(category.displayOrder) || 0,
|
||||||
|
icon: category.icon || null,
|
||||||
|
status: category.status || 'Active',
|
||||||
};
|
};
|
||||||
await api.update(payload);
|
await api.update(payload);
|
||||||
} else {
|
} else {
|
||||||
|
// CreateTagRequest
|
||||||
const payload = {
|
const payload = {
|
||||||
name: category.name,
|
tenantId: user?.tenantId ?? undefined,
|
||||||
description: category.description,
|
tagName: category.name,
|
||||||
status: category.status,
|
typeId: category.typeId || null,
|
||||||
|
parentTagId: category.parentTagId?.length ? category.parentTagId : [],
|
||||||
|
slug: category.slug || toSlug(category.name),
|
||||||
|
displayOrder: Number(category.displayOrder) || 0,
|
||||||
|
icon: category.icon || null,
|
||||||
};
|
};
|
||||||
await api.create(payload);
|
await api.create(payload);
|
||||||
}
|
}
|
||||||
@@ -60,6 +111,18 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const typeOptions = types.map(t => ({
|
||||||
|
id: t.id || t._id,
|
||||||
|
label: t.typeName || '(no name)',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const tagOptions = allTags
|
||||||
|
.filter(t => (t._id || t.id) !== (category._Id || category.id)) // evita ser su propio padre
|
||||||
|
.map(t => ({
|
||||||
|
id: t.id || t._id,
|
||||||
|
label: t.tagName || t.name || '(no name)',
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ p: 2 }}>
|
<Paper sx={{ p: 2 }}>
|
||||||
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
<Typography variant="subtitle1" sx={{ mb: 2 }}>
|
||||||
@@ -68,23 +131,93 @@ export default function AddOrEditCategoryForm({ onAdd, initialData, onCancel })
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
name="name"
|
name="name"
|
||||||
label="Name"
|
label="Name *"
|
||||||
value={category.name}
|
value={category.name}
|
||||||
onChange={handleChange}
|
onChange={(e) => updateField('name', e.target.value)}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
name="description"
|
name="slug"
|
||||||
label="Description"
|
label="Slug"
|
||||||
value={category.description}
|
value={category.slug}
|
||||||
onChange={handleChange}
|
onChange={(e) => updateField('slug', e.target.value)}
|
||||||
fullWidth
|
fullWidth
|
||||||
multiline
|
sx={{ mb: 2 }}
|
||||||
minRows={3}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
|
<InputLabel id="type-label">Type *</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="type-label"
|
||||||
|
label="Type *"
|
||||||
|
value={category.typeId}
|
||||||
|
onChange={(e) => updateField('typeId', e.target.value)}
|
||||||
|
>
|
||||||
|
{typeOptions.map((opt) => (
|
||||||
|
<MenuItem key={opt.id} value={opt.id}>{opt.label}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
|
<InputLabel id="parent-label">Parent Categories</InputLabel>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
labelId="parent-label"
|
||||||
|
label="Parent Categories"
|
||||||
|
value={category.parentTagId}
|
||||||
|
onChange={(e) => updateField('parentTagId', e.target.value)}
|
||||||
|
input={<OutlinedInput label="Parent Categories" />}
|
||||||
|
renderValue={(selected) => (
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||||
|
{selected.map((value) => {
|
||||||
|
const match = tagOptions.find(o => o.id === value);
|
||||||
|
return <Chip key={value} label={match?.label ?? value} />;
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tagOptions.map((opt) => (
|
||||||
|
<MenuItem key={opt.id} value={opt.id}>{opt.label}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
name="displayOrder"
|
||||||
|
label="Display Order"
|
||||||
|
type="number"
|
||||||
|
value={category.displayOrder}
|
||||||
|
onChange={(e) => updateField('displayOrder', e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
name="icon"
|
||||||
|
label="Icon (URL)"
|
||||||
|
value={category.icon}
|
||||||
|
onChange={(e) => updateField('icon', e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
|
||||||
|
{category._Id && (
|
||||||
|
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||||
|
<InputLabel id="status-label">Status</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="status-label"
|
||||||
|
label="Status"
|
||||||
|
value={category.status}
|
||||||
|
onChange={(e) => updateField('status', e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value="Active">Active</MenuItem>
|
||||||
|
<MenuItem value="Inactive">Inactive</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box display="flex" justifyContent="flex-end" gap={1} mt={3}>
|
<Box display="flex" justifyContent="flex-end" gap={1} mt={3}>
|
||||||
<Button onClick={onCancel} className="button-transparent">Cancel</Button>
|
<Button onClick={onCancel} className="button-transparent">Cancel</Button>
|
||||||
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
|
<Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button>
|
||||||
|
|||||||
@@ -47,8 +47,12 @@ export default function Categories() {
|
|||||||
setEditingCategory({
|
setEditingCategory({
|
||||||
_Id: r._id || r._Id || '',
|
_Id: r._id || r._Id || '',
|
||||||
id: r.id || r.Id || '',
|
id: r.id || r.Id || '',
|
||||||
name: r.name ?? '',
|
name: r.tagName ?? r.name ?? '',
|
||||||
description: r.description ?? '',
|
slug: r.slug ?? '',
|
||||||
|
typeId: r.typeId ?? '',
|
||||||
|
parentTagId: Array.isArray(r.parentTagId) ? r.parentTagId : [],
|
||||||
|
displayOrder: Number.isFinite(r.displayOrder) ? r.displayOrder : 0,
|
||||||
|
icon: r.icon ?? '',
|
||||||
status: r.status ?? 'Active',
|
status: r.status ?? 'Active',
|
||||||
});
|
});
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
@@ -62,14 +66,10 @@ export default function Categories() {
|
|||||||
const confirmDelete = async () => {
|
const confirmDelete = async () => {
|
||||||
try {
|
try {
|
||||||
if (!rowToDelete) return;
|
if (!rowToDelete) return;
|
||||||
const payload = {
|
await api.changeStatus({
|
||||||
_Id: rowToDelete._id || rowToDelete._Id,
|
id: rowToDelete.id || rowToDelete.Id || rowToDelete._id || rowToDelete._Id,
|
||||||
id: rowToDelete.id || rowToDelete.Id || '',
|
status: 'Inactive',
|
||||||
name: rowToDelete.name,
|
});
|
||||||
description: rowToDelete.description,
|
|
||||||
status: 'Inactive', // soft-delete
|
|
||||||
};
|
|
||||||
await api.update(payload);
|
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Delete failed:', e);
|
console.error('Delete failed:', e);
|
||||||
@@ -86,8 +86,9 @@ export default function Categories() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: 'name', headerName: 'Name', flex: 1, minWidth: 200 },
|
{ field: 'tagName', headerName: 'Name', flex: 1, minWidth: 200, valueGetter: (p) => p.row?.tagName ?? p.row?.name },
|
||||||
{ field: 'description', headerName: 'Description', flex: 1, minWidth: 250 },
|
{ field: 'slug', headerName: 'Slug', width: 220 },
|
||||||
|
{ field: 'displayOrder', headerName: 'Display', width: 120, valueGetter: (p) => p.row?.displayOrder ?? 0 },
|
||||||
{ field: 'status', headerName: 'Status', width: 140, valueGetter: (p) => p.row?.status ?? 'Active' },
|
{ field: 'status', headerName: 'Status', width: 140, valueGetter: (p) => p.row?.status ?? 'Active' },
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
|
|||||||
Reference in New Issue
Block a user