224 lines
No EOL
6.4 KiB
TypeScript
224 lines
No EOL
6.4 KiB
TypeScript
/**
|
|
* This file is part of Sharenet.
|
|
*
|
|
* Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
|
*
|
|
* You may obtain a copy of the license at:
|
|
* https://creativecommons.org/licenses/by-nc-sa/4.0/
|
|
*
|
|
* Copyright (c) 2024 Continuist <continuist02@gmail.com>
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { productApi, Product } from '@/lib/api';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ResponsiveTable } from '@/components/ui/responsive-table';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
export default function ProductsPage() {
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
description: '',
|
|
});
|
|
const [errors, setErrors] = useState({
|
|
name: '',
|
|
description: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
loadProducts();
|
|
}, []);
|
|
|
|
const loadProducts = async () => {
|
|
try {
|
|
const response = await productApi.getAll();
|
|
setProducts(response.data);
|
|
} catch (error) {
|
|
console.error('Error loading products:', error);
|
|
}
|
|
};
|
|
|
|
const validateForm = () => {
|
|
const newErrors = {
|
|
name: '',
|
|
description: '',
|
|
};
|
|
|
|
// Product name cannot be empty
|
|
if (!formData.name.trim()) {
|
|
newErrors.name = 'Product name is required';
|
|
}
|
|
|
|
// Description can be empty, no validation needed
|
|
setErrors(newErrors);
|
|
return !newErrors.name;
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (editingProduct) {
|
|
await productApi.update(editingProduct.id, formData);
|
|
} else {
|
|
await productApi.create(formData);
|
|
}
|
|
setIsDialogOpen(false);
|
|
setEditingProduct(null);
|
|
setFormData({ name: '', description: '' });
|
|
setErrors({ name: '', description: '' });
|
|
loadProducts();
|
|
} catch (error) {
|
|
console.error('Error saving product:', error);
|
|
}
|
|
};
|
|
|
|
const handleEdit = (product: Product) => {
|
|
setEditingProduct(product);
|
|
setFormData({
|
|
name: product.name,
|
|
description: product.description,
|
|
});
|
|
setErrors({ name: '', description: '' });
|
|
setIsDialogOpen(true);
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (confirm('Are you sure you want to delete this product?')) {
|
|
try {
|
|
await productApi.delete(id);
|
|
loadProducts();
|
|
} catch (error) {
|
|
console.error('Error deleting product:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleInputChange = (field: 'name' | 'description', value: string) => {
|
|
setFormData({ ...formData, [field]: value });
|
|
// Clear error when user starts typing
|
|
if (errors[field]) {
|
|
setErrors({ ...errors, [field]: '' });
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
key: 'name',
|
|
header: 'Name',
|
|
mobilePriority: true,
|
|
},
|
|
{
|
|
key: 'description',
|
|
header: 'Description',
|
|
mobilePriority: true,
|
|
render: (value: unknown) => String(value || 'No description'),
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: 'Actions',
|
|
mobilePriority: true,
|
|
render: (_: unknown, product: Record<string, unknown>) => (
|
|
<div className="flex flex-col sm:flex-row gap-2 sm:gap-1">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleEdit(product as unknown as Product)}
|
|
className="w-full sm:w-auto"
|
|
>
|
|
Edit
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={() => handleDelete((product as unknown as Product).id)}
|
|
className="w-full sm:w-auto"
|
|
>
|
|
Delete
|
|
</Button>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
|
<h1 className="text-2xl font-bold">Products</h1>
|
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
onClick={() => {
|
|
setEditingProduct(null);
|
|
setFormData({ name: '', description: '' });
|
|
setErrors({ name: '', description: '' });
|
|
}}
|
|
className="w-full sm:w-auto"
|
|
>
|
|
Add Product
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>{editingProduct ? 'Edit Product' : 'Add Product'}</DialogTitle>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Name *</Label>
|
|
<Input
|
|
id="name"
|
|
value={formData.name}
|
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
|
className={errors.name ? 'border-red-500' : ''}
|
|
/>
|
|
{errors.name && (
|
|
<p className="text-red-500 text-sm">{errors.name}</p>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="description">Description</Label>
|
|
<Input
|
|
id="description"
|
|
value={formData.description}
|
|
onChange={(e) => handleInputChange('description', e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row gap-2 pt-2">
|
|
<Button type="submit" className="flex-1">
|
|
{editingProduct ? 'Update' : 'Create'}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setIsDialogOpen(false)}
|
|
className="flex-1"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
<ResponsiveTable columns={columns} data={products as unknown as Record<string, unknown>[]} />
|
|
</div>
|
|
);
|
|
}
|