Initial commit - PromptTech
This commit is contained in:
505
docs/guides/USAGE_GUIDE.md
Normal file
505
docs/guides/USAGE_GUIDE.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# Refactored Code Usage Guide
|
||||
|
||||
Quick reference for developers working with the refactored codebase.
|
||||
|
||||
## Backend Helper Functions
|
||||
|
||||
### 1. CRUD Helpers
|
||||
|
||||
#### `_get_or_404(db, model, record_id, error_message)`
|
||||
|
||||
Fetch a record by ID or raise 404 error.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
result = await db.execute(select(Product).where(Product.id == product_id))
|
||||
product = result.scalar_one_or_none()
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Product not found")
|
||||
|
||||
# After
|
||||
product = await _get_or_404(db, Product, product_id, "Product not found")
|
||||
```
|
||||
|
||||
#### `_soft_delete(db, record, commit=True)`
|
||||
|
||||
Soft delete a record (set `is_active=False`).
|
||||
|
||||
**Usage:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
product.is_active = False
|
||||
await db.commit()
|
||||
return {"message": "Product deleted"}
|
||||
|
||||
# After
|
||||
return await _soft_delete(db, product)
|
||||
```
|
||||
|
||||
#### `_build_response(message, **kwargs)`
|
||||
|
||||
Build standardized API responses.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
return {"message": "Success", "id": product.id, "status": "active"}
|
||||
|
||||
# After
|
||||
return _build_response("Success", id=product.id, status="active")
|
||||
```
|
||||
|
||||
### 2. Serialization Helpers
|
||||
|
||||
#### `_safe_isoformat(dt)`
|
||||
|
||||
Safely convert datetime to ISO format string.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
"created_at": product.created_at.isoformat() if product.created_at else None
|
||||
|
||||
# After
|
||||
"created_at": _safe_isoformat(product.created_at)
|
||||
```
|
||||
|
||||
#### `_safe_enum_value(enum_val, default="pending")`
|
||||
|
||||
Safely extract enum value.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
"status": order.status.value if order.status else "pending"
|
||||
|
||||
# After
|
||||
"status": _safe_enum_value(order.status)
|
||||
```
|
||||
|
||||
#### `_calculate_reviews_stats(reviews)`
|
||||
|
||||
Calculate review statistics.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
if reviews:
|
||||
total = len(reviews)
|
||||
avg = sum(r.rating for r in reviews) / total
|
||||
stats = {"average_rating": round(avg, 2), "total_reviews": total}
|
||||
else:
|
||||
stats = {"average_rating": 0, "total_reviews": 0}
|
||||
|
||||
# After
|
||||
stats = _calculate_reviews_stats(reviews)
|
||||
```
|
||||
|
||||
### 3. Query Optimization Patterns
|
||||
|
||||
#### Batched Queries
|
||||
|
||||
**Before:**
|
||||
|
||||
```python
|
||||
total_users = await db.execute(select(func.count(User.id)))
|
||||
total_users = total_users.scalar()
|
||||
total_products = await db.execute(select(func.count(Product.id)))
|
||||
total_products = total_products.scalar()
|
||||
# ... 8 more queries
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```python
|
||||
counts_result = await db.execute(
|
||||
select(
|
||||
func.count(distinct(User.id)).label('users'),
|
||||
func.count(distinct(Product.id)).label('products'),
|
||||
func.count(distinct(Service.id)).label('services'),
|
||||
func.count(distinct(Order.id)).label('orders')
|
||||
)
|
||||
)
|
||||
counts = counts_result.one()
|
||||
stats = {
|
||||
"total_users": int(counts.users),
|
||||
"total_products": int(counts.products),
|
||||
"total_services": int(counts.services),
|
||||
"total_orders": int(counts.orders)
|
||||
}
|
||||
```
|
||||
|
||||
#### Streamlined Filtering
|
||||
|
||||
**Before:**
|
||||
|
||||
```python
|
||||
query = select(Order).where(Order.status != OrderStatus.CANCELLED)
|
||||
query = query.where(Order.status != OrderStatus.REFUNDED)
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```python
|
||||
valid_statuses = [s for s in OrderStatus if s not in (OrderStatus.CANCELLED, OrderStatus.REFUNDED)]
|
||||
query = select(Order).where(Order.status.in_(valid_statuses))
|
||||
```
|
||||
|
||||
## Frontend Custom Hooks
|
||||
|
||||
### 1. useAdminAPI Hook
|
||||
|
||||
Centralized API call management with error handling.
|
||||
|
||||
**Import:**
|
||||
|
||||
```javascript
|
||||
import { useAdminAPI } from '../hooks/useAdminAPI';
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```javascript
|
||||
function AdminDashboard() {
|
||||
const { token } = useAuth();
|
||||
const { loading, apiGet, apiPost, apiPut, apiDelete } = useAdminAPI(token);
|
||||
|
||||
// GET request
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const data = await apiGet('/admin/dashboard', 'Failed to load dashboard');
|
||||
setDashboardData(data);
|
||||
} catch (error) {
|
||||
// Error already handled by hook (toast shown, navigation handled)
|
||||
}
|
||||
};
|
||||
|
||||
// POST request
|
||||
const createProduct = async (productData) => {
|
||||
try {
|
||||
const result = await apiPost('/admin/products', productData, 'Failed to create product');
|
||||
toast.success('Product created!');
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Error handled
|
||||
}
|
||||
};
|
||||
|
||||
// PUT request
|
||||
const updateProduct = async (id, data) => {
|
||||
await apiPut(`/admin/products/${id}`, data, 'Failed to update');
|
||||
};
|
||||
|
||||
// DELETE request
|
||||
const deleteProduct = async (id) => {
|
||||
await apiDelete(`/admin/products/${id}`, 'Failed to delete');
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- ✅ Automatic 10-second timeout
|
||||
- ✅ Consistent error handling
|
||||
- ✅ Auto-navigation on 401/403
|
||||
- ✅ Toast notifications
|
||||
- ✅ Loading state management
|
||||
|
||||
### 2. useDialog Hook
|
||||
|
||||
Simplified dialog/modal state management.
|
||||
|
||||
**Import:**
|
||||
|
||||
```javascript
|
||||
import { useDialog } from '../hooks/useDialogState';
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```javascript
|
||||
function ProductsTab() {
|
||||
const { isOpen, item, open, close } = useDialog();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => open()}>Add Product</Button>
|
||||
<Button onClick={() => open(product)}>Edit Product</Button>
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={close}>
|
||||
<DialogContent>
|
||||
{item ? (
|
||||
<h2>Edit {item.name}</h2>
|
||||
) : (
|
||||
<h2>Add New Product</h2>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. useFormState Hook
|
||||
|
||||
Manage form state efficiently.
|
||||
|
||||
**Import:**
|
||||
|
||||
```javascript
|
||||
import { useFormState } from '../hooks/useDialogState';
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```javascript
|
||||
function ProductForm({ initialData }) {
|
||||
const { form, updateField, updateForm, resetForm } = useFormState(
|
||||
initialData || { name: '', price: 0, stock: 0 }
|
||||
);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<input
|
||||
value={form.name}
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
value={form.price}
|
||||
onChange={(e) => updateField('price', parseFloat(e.target.value))}
|
||||
/>
|
||||
<Button onClick={resetForm}>Reset</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Complete CRUD Endpoint (Refactored)
|
||||
|
||||
```python
|
||||
# List with optional filtering
|
||||
@api_router.get("/admin/products")
|
||||
async def admin_get_products(
|
||||
include_inactive: bool = False,
|
||||
user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
query = select(Product)
|
||||
if not include_inactive:
|
||||
query = query.where(Product.is_active == True)
|
||||
result = await db.execute(query)
|
||||
products = result.scalars().all()
|
||||
return [product_to_dict(p) for p in products]
|
||||
|
||||
# Create
|
||||
@api_router.post("/admin/products")
|
||||
async def admin_create_product(
|
||||
product_data: ProductCreate,
|
||||
user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
product = Product(**product_data.model_dump())
|
||||
db.add(product)
|
||||
await db.commit()
|
||||
await db.refresh(product)
|
||||
return product_to_dict(product)
|
||||
|
||||
# Update
|
||||
@api_router.put("/admin/products/{product_id}")
|
||||
async def admin_update_product(
|
||||
product_id: str,
|
||||
product_data: ProductUpdate,
|
||||
user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
product = await _get_or_404(db, Product, product_id, "Product not found")
|
||||
update_data = product_data.model_dump(exclude_unset=True)
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(product, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(product)
|
||||
return product_to_dict(product)
|
||||
|
||||
# Delete (soft)
|
||||
@api_router.delete("/admin/products/{product_id}")
|
||||
async def admin_delete_product(
|
||||
product_id: str,
|
||||
user: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
product = await _get_or_404(db, Product, product_id, "Product not found")
|
||||
return await _soft_delete(db, product)
|
||||
```
|
||||
|
||||
### Complete React Component (Refactored)
|
||||
|
||||
```javascript
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAdminAPI } from '../hooks/useAdminAPI';
|
||||
import { useDialog, useFormState } from '../hooks/useDialogState';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
function ProductsTab() {
|
||||
const { token } = useAuth();
|
||||
const { loading, apiGet, apiPost, apiPut, apiDelete } = useAdminAPI(token);
|
||||
const { isOpen, item, open, close } = useDialog();
|
||||
const { form, updateField, resetForm } = useFormState({
|
||||
name: '', price: 0, stock: 0, category: 'electronics'
|
||||
});
|
||||
|
||||
const [products, setProducts] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
}, []);
|
||||
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
const data = await apiGet('/admin/products', 'Failed to load products');
|
||||
setProducts(data);
|
||||
} catch (error) {
|
||||
// Error handled by hook
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (item) {
|
||||
await apiPut(`/admin/products/${item.id}`, form, 'Failed to update');
|
||||
} else {
|
||||
await apiPost('/admin/products', form, 'Failed to create');
|
||||
}
|
||||
await fetchProducts();
|
||||
close();
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
// Error handled
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
await apiDelete(`/admin/products/${id}`, 'Failed to delete');
|
||||
await fetchProducts();
|
||||
} catch (error) {
|
||||
// Error handled
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={() => open()}>Add Product</Button>
|
||||
{loading && <Spinner />}
|
||||
{/* Product list and dialog */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Refactored Code
|
||||
|
||||
### Run Full Test Suite
|
||||
|
||||
```bash
|
||||
./test_refactoring.sh
|
||||
```
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
- [ ] Dashboard loads in < 200ms
|
||||
- [ ] CRUD operations work for products
|
||||
- [ ] CRUD operations work for services
|
||||
- [ ] Inventory adjustments log correctly
|
||||
- [ ] Order status updates properly
|
||||
- [ ] Error handling shows appropriate messages
|
||||
- [ ] 404 errors return correct format
|
||||
- [ ] Auth errors redirect to login
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Operation | Target | Status |
|
||||
|-----------|--------|--------|
|
||||
| Dashboard Load | < 200ms | ✅ 50ms |
|
||||
| Product List | < 100ms | ✅ 35ms |
|
||||
| Single Update | < 80ms | ✅ 55ms |
|
||||
| Batch Operations | < 500ms | ✅ 120ms |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backend Issues
|
||||
|
||||
**Import Errors:**
|
||||
|
||||
```python
|
||||
# Make sure all imports are at the top
|
||||
from typing import Optional
|
||||
from sqlalchemy import select, func, distinct
|
||||
```
|
||||
|
||||
**Async/Await:**
|
||||
|
||||
```python
|
||||
# Always await async functions
|
||||
product = await _get_or_404(db, Product, id) # ✅ Correct
|
||||
product = _get_or_404(db, Product, id) # ❌ Wrong
|
||||
```
|
||||
|
||||
### Frontend Issues
|
||||
|
||||
**Hook Rules:**
|
||||
|
||||
```javascript
|
||||
// ✅ Call hooks at top level
|
||||
const { apiGet } = useAdminAPI(token);
|
||||
|
||||
// ❌ Don't call in callbacks
|
||||
onClick={() => useAdminAPI(token)} // Wrong!
|
||||
```
|
||||
|
||||
**Error Handling:**
|
||||
|
||||
```javascript
|
||||
// Errors are automatically handled by useAdminAPI
|
||||
// But you can still catch for custom logic
|
||||
try {
|
||||
await apiGet('/endpoint');
|
||||
} catch (error) {
|
||||
// Do something additional
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When adding new endpoints, follow these patterns:
|
||||
|
||||
Backend:
|
||||
|
||||
- [ ] Use `_get_or_404()` for record fetching
|
||||
- [ ] Use `_soft_delete()` for deletions
|
||||
- [ ] Use `_build_response()` for responses
|
||||
- [ ] Use serialization helpers in `*_to_dict()`
|
||||
- [ ] Batch queries when fetching multiple counts
|
||||
- [ ] Add proper error handling
|
||||
|
||||
Frontend:
|
||||
|
||||
- [ ] Use `useAdminAPI` for all API calls
|
||||
- [ ] Use `useDialog` for modal state
|
||||
- [ ] Use `useFormState` for form management
|
||||
- [ ] Add loading states
|
||||
- [ ] Handle errors gracefully
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Check [REFACTORING_REPORT.md](REFACTORING_REPORT.md) for detailed explanations.
|
||||
Reference in New Issue
Block a user