ADD: Administrador de productos(WIP)
This commit is contained in:
parent
2bd5d00827
commit
83f0abff13
114
src/components/ui/Input.vue
Normal file
114
src/components/ui/Input.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
/** Props */
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
});
|
||||
|
||||
/** Emits */
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'change', 'focus', 'blur', 'keydown', 'keyup']);
|
||||
|
||||
/** Función para concatenar clases (equivalente a cn() de React) */
|
||||
const cn = (...classes) => {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
};
|
||||
|
||||
/** Clases computadas */
|
||||
const inputClasses = computed(() => {
|
||||
return cn(
|
||||
"flex h-10 w-full rounded-md border border-input px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
props.class
|
||||
);
|
||||
});
|
||||
|
||||
/** Manejadores de eventos */
|
||||
const handleInput = (event) => {
|
||||
emit('update:modelValue', event.target.value);
|
||||
emit('input', event);
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
emit('change', event);
|
||||
};
|
||||
|
||||
const handleFocus = (event) => {
|
||||
emit('focus', event);
|
||||
};
|
||||
|
||||
const handleBlur = (event) => {
|
||||
emit('blur', event);
|
||||
};
|
||||
|
||||
const handleKeydown = (event) => {
|
||||
emit('keydown', event);
|
||||
};
|
||||
|
||||
const handleKeyup = (event) => {
|
||||
emit('keyup', event);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
:id="id"
|
||||
:name="name"
|
||||
:type="type"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:required="required"
|
||||
:class="inputClasses"
|
||||
@input="handleInput"
|
||||
@change="handleChange"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@keydown="handleKeydown"
|
||||
@keyup="handleKeyup"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/*
|
||||
Nota: Las clases CSS están definidas en Tailwind CSS.
|
||||
Si necesitas definir estilos personalizados para las clases como
|
||||
'border-input', 'bg-background', 'ring-ring', etc.,
|
||||
puedes agregarlas aquí o en tu archivo de configuración de Tailwind.
|
||||
*/
|
||||
</style>
|
||||
@ -100,6 +100,11 @@ onMounted(() => {
|
||||
name="Clasificaciones Comerciales"
|
||||
to="admin.comercial-classifications.index"
|
||||
/>
|
||||
<Link
|
||||
icon="inventory"
|
||||
name="Productos"
|
||||
to="admin.products.index"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section name="Capacitaciones">
|
||||
|
||||
229
src/pages/Products/Index.vue
Normal file
229
src/pages/Products/Index.vue
Normal file
@ -0,0 +1,229 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Button from '@Holos/Button/Button.vue';
|
||||
import Card from '@Holos/Card/Card.vue';
|
||||
import CardContent from '@Holos/Card/CardContent.vue';
|
||||
import CardHeader from '@Holos/Card/CardHeader.vue';
|
||||
import CardTitle from '@Holos/Card/CardTitle.vue';
|
||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
import Table from '@Holos/Table.vue';
|
||||
import IconButton from '@Holos/Button/Icon.vue';
|
||||
|
||||
import { can, apiTo, viewTo, transl } from './Module'
|
||||
import { useSearcher } from '@Services/Api';
|
||||
import Input from '@Components/ui/Input.vue';
|
||||
|
||||
const models = ref([]);
|
||||
const router = useRouter();
|
||||
|
||||
const searcher = useSearcher({
|
||||
url: apiTo('index'),
|
||||
onSuccess: (r) => {
|
||||
console.log('Datos recibidos:', r);
|
||||
// Según la estructura que muestras, los productos están en r.data.products.data
|
||||
models.value = r.data?.products?.data || r.products || [];
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error cargando productos:', error);
|
||||
models.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
/** Función para formatear atributos */
|
||||
const formatAttributes = (attributes) => {
|
||||
if (!attributes) return '-';
|
||||
|
||||
const formatted = Object.entries(attributes).map(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return `${key}`;
|
||||
}
|
||||
return `${key}`;
|
||||
}).join(' | ');
|
||||
|
||||
return formatted.length > 50 ? formatted.substring(0, 50) + '...' : formatted;
|
||||
};
|
||||
|
||||
/** Función para mostrar clasificaciones */
|
||||
const formatClassifications = (classifications) => {
|
||||
if (!classifications || classifications.length === 0) return '-';
|
||||
return classifications.map(c => c.name).join(', ');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
searcher.search();
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Productos</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Gestión del catálogo de productos
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button color="info" @click="$router.push({ name: 'products.create' })">
|
||||
Nuevo Producto
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent class="p-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<GoogleIcon class="w-8 h-8 text-primary" name="inventory_2" />
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-metric-value">150</p>
|
||||
<p class="text-sm text-muted-foreground">Total Productos</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Catálogo de Productos</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex items-center space-x-4 mb-6">
|
||||
<div class="relative flex-1 max-w-sm">
|
||||
<GoogleIcon class="absolute left-3 top-2 h-4 w-4 text-muted-foreground" name="search" />
|
||||
<Input
|
||||
placeholder="Buscar productos..."
|
||||
class="pl-10"
|
||||
@input="(e) => searcher.search({ search: e.target.value })"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" class="flex items-center">
|
||||
<GoogleIcon class="w-4 h-4 mr-2" name="filter_list" />
|
||||
Filtros
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
:items="models"
|
||||
:processing="searcher.processing"
|
||||
@send-pagination="(page) => searcher.pagination(page)"
|
||||
>
|
||||
<template #head>
|
||||
<th>{{ $t('code') }}</th>
|
||||
<th>SKU</th>
|
||||
<th>{{ $t('name') }}</th>
|
||||
<th>{{ $t('description') }}</th>
|
||||
<th>Atributos</th>
|
||||
<th>Clasificaciones</th>
|
||||
<th class="w-20 text-center">{{ $t('status') }}</th>
|
||||
<th class="w-32 text-center">{{ $t('actions') }}</th>
|
||||
</template>
|
||||
|
||||
<template #body="{ items }">
|
||||
<tr v-for="product in items" :key="product.id" class="table-row">
|
||||
<td class="table-cell">
|
||||
<div class="flex items-center">
|
||||
<GoogleIcon name="inventory" class="text-blue-500 mr-2" />
|
||||
<code class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
|
||||
{{ product.code }}
|
||||
</code>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="table-cell">
|
||||
<span class="font-mono text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ product.sku }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="table-cell">
|
||||
<span class="font-semibold">{{ product.name }}</span>
|
||||
</td>
|
||||
|
||||
<td class="table-cell">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ product.description || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="table-cell">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400" :title="JSON.stringify(product.attributes)">
|
||||
{{ formatAttributes(product.attributes) }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="table-cell">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="classification in product.classifications"
|
||||
:key="classification.id"
|
||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100"
|
||||
>
|
||||
{{ classification.name }}
|
||||
</span>
|
||||
<span v-if="!product.classifications || product.classifications.length === 0" class="text-gray-400 text-xs">
|
||||
Sin clasificar
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="table-cell text-center">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
|
||||
:class="product.is_active
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'"
|
||||
>
|
||||
{{ product.is_active ? $t('active') : $t('inactive') }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="table-cell">
|
||||
<div class="table-actions">
|
||||
<IconButton
|
||||
icon="visibility"
|
||||
:title="$t('crud.show')"
|
||||
outline
|
||||
/>
|
||||
<IconButton
|
||||
icon="edit"
|
||||
:title="$t('crud.edit')"
|
||||
outline
|
||||
@click="$router.push({ name: 'products.edit', params: { id: product.id } })"
|
||||
/>
|
||||
<IconButton
|
||||
icon="delete"
|
||||
:title="$t('crud.destroy')"
|
||||
outline
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<td class="table-cell" colspan="8">
|
||||
<div class="flex flex-col items-center text-center py-8">
|
||||
<GoogleIcon name="inventory" class="text-4xl text-gray-400 mb-2" />
|
||||
<p class="font-semibold text-gray-600 dark:text-gray-400">
|
||||
{{ $t('registers.empty') }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500">
|
||||
No se encontraron productos
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
23
src/pages/Products/Module.js
Normal file
23
src/pages/Products/Module.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { lang } from '@Lang/i18n';
|
||||
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||
|
||||
// Ruta API
|
||||
const apiTo = (name, params = {}) => route(`products.${name}`, params)
|
||||
|
||||
// Ruta visual
|
||||
const viewTo = ({ name = '', params = {}, query = {} }) => view({
|
||||
name: `admin.products.${name}`, params, query
|
||||
})
|
||||
|
||||
// Obtener traducción del componente
|
||||
const transl = (str) => lang(`admin.products.${str}`)
|
||||
|
||||
// Control de permisos
|
||||
const can = (permission) => hasPermission(`admin.products.${permission}`)
|
||||
|
||||
export {
|
||||
can,
|
||||
viewTo,
|
||||
apiTo,
|
||||
transl
|
||||
}
|
||||
300
src/pages/Products/a.jsx
Normal file
300
src/pages/Products/a.jsx
Normal file
@ -0,0 +1,300 @@
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
Edit,
|
||||
Trash2,
|
||||
Package,
|
||||
MoreHorizontal
|
||||
} from "lucide-react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
// Mock data
|
||||
const products = [
|
||||
{
|
||||
id: 1,
|
||||
code: "PROD-001",
|
||||
name: "Laptop Dell Inspiron 15",
|
||||
description: "Laptop empresarial de alto rendimiento",
|
||||
category: "Electrónicos > Computadoras",
|
||||
brand: "Dell",
|
||||
price: 15999.00,
|
||||
stock: 45,
|
||||
minStock: 10,
|
||||
status: "active",
|
||||
lastUpdated: "2024-01-15"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
code: "PROD-002",
|
||||
name: "Mouse Inalámbrico Logitech",
|
||||
description: "Mouse óptico inalámbrico con receptor USB",
|
||||
category: "Electrónicos > Accesorios",
|
||||
brand: "Logitech",
|
||||
price: 599.00,
|
||||
stock: 120,
|
||||
minStock: 25,
|
||||
status: "active",
|
||||
lastUpdated: "2024-01-14"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
code: "PROD-003",
|
||||
name: "Monitor LG 24 pulgadas",
|
||||
description: "Monitor LED Full HD con conexión HDMI",
|
||||
category: "Electrónicos > Monitores",
|
||||
brand: "LG",
|
||||
price: 3299.00,
|
||||
stock: 8,
|
||||
minStock: 15,
|
||||
status: "low-stock",
|
||||
lastUpdated: "2024-01-13"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
code: "PROD-004",
|
||||
name: "Teclado Mecánico RGB",
|
||||
description: "Teclado mecánico para gaming con iluminación RGB",
|
||||
category: "Electrónicos > Accesorios",
|
||||
brand: "Corsair",
|
||||
price: 2199.00,
|
||||
stock: 0,
|
||||
minStock: 5,
|
||||
status: "out-of-stock",
|
||||
lastUpdated: "2024-01-12"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
code: "PROD-005",
|
||||
name: "Webcam HD Logitech",
|
||||
description: "Cámara web HD para videoconferencias",
|
||||
category: "Electrónicos > Accesorios",
|
||||
brand: "Logitech",
|
||||
price: 899.00,
|
||||
stock: 32,
|
||||
minStock: 10,
|
||||
status: "active",
|
||||
lastUpdated: "2024-01-11"
|
||||
}
|
||||
]
|
||||
|
||||
const getStatusBadge = (status: string, stock: number, minStock: number) => {
|
||||
if (stock === 0) {
|
||||
return <Badge variant="destructive">Sin Stock</Badge>
|
||||
}
|
||||
if (stock <= minStock) {
|
||||
return <Badge className="bg-warning text-warning-foreground">Stock Bajo</Badge>
|
||||
}
|
||||
return <Badge className="bg-success text-success-foreground">Activo</Badge>
|
||||
}
|
||||
|
||||
export default function Products() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [filteredProducts, setFilteredProducts] = useState(products)
|
||||
|
||||
const handleSearch = (term: string) => {
|
||||
setSearchTerm(term)
|
||||
const filtered = products.filter(product =>
|
||||
product.name.toLowerCase().includes(term.toLowerCase()) ||
|
||||
product.code.toLowerCase().includes(term.toLowerCase()) ||
|
||||
product.brand.toLowerCase().includes(term.toLowerCase())
|
||||
)
|
||||
setFilteredProducts(filtered)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Productos</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Gestión del catálogo de productos
|
||||
</p>
|
||||
</div>
|
||||
<Button className="bg-primary hover:bg-primary-hover">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Nuevo Producto
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Package className="w-8 h-8 text-primary" />
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-metric-value">{products.length}</p>
|
||||
<p className="text-sm text-muted-foreground">Total Productos</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 rounded-full bg-success flex items-center justify-center">
|
||||
<span className="text-success-foreground font-bold text-sm">✓</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-success">
|
||||
{products.filter(p => p.status === "active").length}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Activos</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 rounded-full bg-warning flex items-center justify-center">
|
||||
<span className="text-warning-foreground font-bold text-sm">!</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-warning">
|
||||
{products.filter(p => p.status === "low-stock").length}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Stock Bajo</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 rounded-full bg-destructive flex items-center justify-center">
|
||||
<span className="text-destructive-foreground font-bold text-sm">×</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-destructive">
|
||||
{products.filter(p => p.status === "out-of-stock").length}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Sin Stock</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters and Search */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Catálogo de Productos</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-4 mb-6">
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar productos..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filtros
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Products Table */}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Código</TableHead>
|
||||
<TableHead>Producto</TableHead>
|
||||
<TableHead>Categoría</TableHead>
|
||||
<TableHead>Marca</TableHead>
|
||||
<TableHead>Precio</TableHead>
|
||||
<TableHead>Stock</TableHead>
|
||||
<TableHead>Estado</TableHead>
|
||||
<TableHead className="w-10"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredProducts.map((product) => (
|
||||
<TableRow key={product.id}>
|
||||
<TableCell className="font-mono text-sm">
|
||||
{product.code}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="font-medium">{product.name}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{product.category}
|
||||
</TableCell>
|
||||
<TableCell>{product.brand}</TableCell>
|
||||
<TableCell className="font-mono">
|
||||
${product.price.toLocaleString('es-MX')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">{product.stock}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Min: {product.minStock}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getStatusBadge(product.status, product.stock, product.minStock)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Package className="w-4 h-4 mr-2" />
|
||||
Ver Stock
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-destructive">
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Eliminar
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -528,6 +528,40 @@ const router = createRouter({
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'products',
|
||||
name: 'admin.products',
|
||||
meta: {
|
||||
title: 'Productos',
|
||||
icon: 'inventory',
|
||||
},
|
||||
redirect: '/admin/products',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'admin.products.index',
|
||||
component: () => import('@Pages/Products/Index.vue'),
|
||||
},
|
||||
/* {
|
||||
path: 'create',
|
||||
name: 'admin.products.create',
|
||||
component: () => import('@Pages/Products/Create.vue'),
|
||||
meta: {
|
||||
title: 'Crear',
|
||||
icon: 'add',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':id/edit',
|
||||
name: 'admin.products.edit',
|
||||
component: () => import('@Pages/Products/Edit.vue'),
|
||||
meta: {
|
||||
title: 'Editar',
|
||||
icon: 'edit',
|
||||
},
|
||||
} */
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'roles',
|
||||
name: 'admin.roles',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user