DEAD_CODE_CLEAN #7
8 changed files with 118 additions and 1 deletions
|
|
@ -6,6 +6,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|||
import { AppContainer, MyText, tw, MyButton, useManualRefresh, SearchBar, useMarkDataFetchers, MyFlatList } from 'common-ui';
|
||||
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { SuccessToast, ErrorToast } from '@/services/toaster';
|
||||
import type { AdminSku } from '@packages/shared';
|
||||
|
||||
type FilterType = 'all' | 'in-stock' | 'out-of-stock';
|
||||
|
|
@ -24,6 +25,16 @@ export default function Products() {
|
|||
|
||||
const { data: productsData, isLoading, error, refetch } = trpc.admin.product.getProducts.useQuery();
|
||||
|
||||
const toggleOutOfStock = trpc.admin.product.toggleOutOfStock.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
await refetch();
|
||||
SuccessToast(res.message);
|
||||
},
|
||||
onError: (err) => {
|
||||
ErrorToast(err.message || 'Failed to update stock status');
|
||||
},
|
||||
});
|
||||
|
||||
useManualRefresh(refetch);
|
||||
|
||||
useMarkDataFetchers(() => {
|
||||
|
|
@ -61,6 +72,10 @@ export default function Products() {
|
|||
router.push(`/(drawer)/dashboard/products/detail/${productId}` as any);
|
||||
};
|
||||
|
||||
const handleToggleStock = (product: { id: number }) => {
|
||||
toggleOutOfStock.mutate({ id: product.id });
|
||||
};
|
||||
|
||||
const FilterButton = ({ filter, label, count }: { filter: FilterType; label: string; count: number }) => (
|
||||
<TouchableOpacity
|
||||
onPress={() => setActiveFilter(filter)}
|
||||
|
|
@ -193,6 +208,17 @@ export default function Products() {
|
|||
<MaterialIcons name="edit" size={16} color="white" />
|
||||
<MyText style={tw`text-white font-semibold ml-1`}>Edit</MyText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => handleToggleStock(product)}
|
||||
disabled={toggleOutOfStock.isPending && toggleOutOfStock.variables?.id === product.id}
|
||||
style={tw`flex-1 ${isOut ? 'bg-green-500' : 'bg-orange-500'} p-3 rounded-lg flex-row items-center justify-center ${toggleOutOfStock.isPending && toggleOutOfStock.variables?.id === product.id ? 'opacity-50' : ''}`}
|
||||
>
|
||||
<MaterialIcons name={isOut ? 'check-circle' : 'block'} size={16} color="white" />
|
||||
<MyText style={tw`text-white font-semibold ml-1`}>
|
||||
{isOut ? 'Stock' : 'Out'}
|
||||
</MyText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useState, useMemo } from 'react'
|
||||
import { createFileRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router'
|
||||
import { Image as ImageIcon, Eye, Pencil, Package } from 'lucide-react'
|
||||
import { Image as ImageIcon, Eye, Pencil, Package, CheckCircle2, Ban } from 'lucide-react'
|
||||
import { AppContainer, p as MyText, MyButton, SearchBar, MyFlatList } from 'web-components'
|
||||
import { trpc } from '@/lib/trpc-client'
|
||||
import { useManualRefresh, useMarkDataFetchers } from '@/lib/refresh-context'
|
||||
import { SuccessToast, ErrorToast } from '@/services/toaster'
|
||||
import type { AdminSku } from '@packages/shared'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/products')({
|
||||
|
|
@ -39,6 +40,16 @@ function Products() {
|
|||
|
||||
const { data: productsData, isLoading, error, refetch } = trpc.admin.product.getProducts.useQuery()
|
||||
|
||||
const toggleOutOfStock = trpc.admin.product.toggleOutOfStock.useMutation({
|
||||
onSuccess: async (res) => {
|
||||
await refetch()
|
||||
SuccessToast(res.message)
|
||||
},
|
||||
onError: (err) => {
|
||||
ErrorToast(err.message || 'Failed to update stock status')
|
||||
},
|
||||
})
|
||||
|
||||
useManualRefresh(refetch)
|
||||
|
||||
useMarkDataFetchers(() => {
|
||||
|
|
@ -70,6 +81,10 @@ function Products() {
|
|||
navigate({ to: '/dashboard/products/$id', params: { id: String(productId) } })
|
||||
}
|
||||
|
||||
const handleToggleStock = (product: { id: number }) => {
|
||||
toggleOutOfStock.mutate({ id: product.id })
|
||||
}
|
||||
|
||||
// Render child routes (new / edit / detail) when not on the list route.
|
||||
// Placed after all hooks so the hook order stays stable across renders.
|
||||
if (location.pathname !== '/dashboard/products') {
|
||||
|
|
@ -191,6 +206,17 @@ function Products() {
|
|||
<Pencil size={16} color="white" />
|
||||
<MyText className="ml-1 font-semibold text-white">Edit</MyText>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleToggleStock(product)}
|
||||
disabled={toggleOutOfStock.isPending && toggleOutOfStock.variables?.id === product.id}
|
||||
className={`flex flex-1 flex-row items-center justify-center rounded-lg p-3 ${
|
||||
isOut ? 'bg-green-500' : 'bg-orange-500'
|
||||
} ${toggleOutOfStock.isPending && toggleOutOfStock.variables?.id === product.id ? 'opacity-50' : ''}`}
|
||||
>
|
||||
{isOut ? <CheckCircle2 size={16} color="white" /> : <Ban size={16} color="white" />}
|
||||
<MyText className="ml-1 font-semibold text-white">{isOut ? 'Stock' : 'Out'}</MyText>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export {
|
|||
updateProductGroup,
|
||||
deleteProductGroup,
|
||||
updateProductPrices,
|
||||
toggleProductOutOfStock,
|
||||
// Merge duplicate products into multi-SKU products
|
||||
mergeDuplicateProducts,
|
||||
// Admin - Slots
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
updateProductGroup as updateProductGroupInDb,
|
||||
deleteProductGroup as deleteProductGroupInDb,
|
||||
updateProductPrices as updateProductPricesInDb,
|
||||
toggleProductOutOfStock as toggleProductOutOfStockInDb,
|
||||
checkProductExistsByName,
|
||||
checkUnitExists,
|
||||
createProduct as createProductInDb,
|
||||
|
|
@ -46,6 +47,7 @@ import type {
|
|||
AdminUpdateSlotProductsResult,
|
||||
AdminSlotsProductIdsResult,
|
||||
AdminUpdateProductPricesResult,
|
||||
AdminToggleOutOfStockResult,
|
||||
} from '@packages/shared'
|
||||
|
||||
|
||||
|
|
@ -178,6 +180,29 @@ export const productRouter = router({
|
|||
}
|
||||
}),
|
||||
|
||||
toggleOutOfStock: protectedProcedure
|
||||
.input(z.object({
|
||||
id: z.number(),
|
||||
}))
|
||||
.mutation(async ({ input }): Promise<AdminToggleOutOfStockResult> => {
|
||||
const { id } = input;
|
||||
|
||||
const result = await toggleProductOutOfStockInDb(id)
|
||||
|
||||
if (!result) {
|
||||
throw new ApiError('Product not found', 404)
|
||||
}
|
||||
|
||||
// Regenerate products/availability cache files so the user app reflects it.
|
||||
await scheduleStoreInitialization()
|
||||
|
||||
return {
|
||||
productId: result.id,
|
||||
isOutOfStock: result.isOutOfStock,
|
||||
message: `Product marked as ${result.isOutOfStock ? 'out of stock' : 'in stock'}`,
|
||||
}
|
||||
}),
|
||||
|
||||
createProduct: protectedProcedure
|
||||
.input(z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
|
|
|
|||
|
|
@ -1576,3 +1576,12 @@ ROOT CAUSE: user product `unitNotation` already contains the amount+unit (e.g. "
|
|||
FIX (match user-ui): drop `formatQuantity` helpers + `productQuantity` prefixes; render `unitNotation` (with productType !== 'combo' guard where user-ui has one). Files: components/ProductCard.tsx, components/AddToCartDialog.tsx, components/FloatingCartBar.tsx, routes/home.product.$id.tsx, routes/flash.tsx, routes/slot-view.tsx, routes/cart.tsx, routes/home.cart.tsx.
|
||||
|
||||
[2026-09-06 04:05:00] VERIFIED web-ui quantity-label fix: grep shows zero formatQuantity/productQuantity render usages left; tsc 0 errors; vite build clean. Label now renders `unitNotation` directly (e.g. Qty: 0.5Kg, 1Kg) matching user-ui.
|
||||
|
||||
[2026-09-06 05:00:00] Feature: product out-of-stock toggle on admin products index (admin-ui + admin-web).
|
||||
- packages/shared/types/admin.ts: add AdminToggleOutOfStockResult { productId, isOutOfStock, message }.
|
||||
- packages/db_helper_sqlite/src/admin-apis/product.ts: add toggleProductOutOfStock(productId) — flips isOutOfStock on ALL SKU rows' productMarketStats (next = !some(out)); returns {id,isOutOfStock}|null.
|
||||
- apps/backend/src/sqliteImporter.ts: export toggleProductOutOfStock.
|
||||
- apps/backend/.../admin-apis/apis/product.ts: add toggleOutOfStock procedure ({id} -> helper -> 404 -> scheduleStoreInitialization -> result). Caches (products.json + availability.json) regenerate.
|
||||
- apps/admin-ui .../products/index.tsx + apps/admin-web .../dashboard.products.tsx: third card button (green "Stock" when out, orange "Out" when live), mutation + refetch + toast.
|
||||
|
||||
[2026-09-06 05:10:00] VERIFIED out-of-stock toggle: packages/db_helper_sqlite index.ts needed explicit export of toggleProductOutOfStock (added). tsc clean: sqlite 0, backend 0, admin-ui 0, admin-web 0; admin-web vite build clean. Note: admin-ui-only toaster + backend sqlite-only DB helper (used by running backend); postgres parity not added.
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export {
|
|||
updateProductGroup,
|
||||
deleteProductGroup,
|
||||
updateProductPrices,
|
||||
toggleProductOutOfStock,
|
||||
} from './src/admin-apis/product'
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -1036,6 +1036,29 @@ export async function updateProductPrices(updates: Array<{
|
|||
return { updatedCount: updates.length, invalidIds: [] }
|
||||
}
|
||||
|
||||
export async function toggleProductOutOfStock(
|
||||
productId: number
|
||||
): Promise<{ id: number; isOutOfStock: boolean } | null> {
|
||||
const skus = await db.query.productSkus.findMany({
|
||||
where: eq(productSkus.productId, productId),
|
||||
with: { marketStats: true },
|
||||
})
|
||||
|
||||
if (skus.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Product is "out" if any of its SKUs is out; toggle the whole product.
|
||||
const nextIsOutOfStock = !skus.some((sku) => sku.marketStats?.isOutOfStock)
|
||||
|
||||
await db
|
||||
.update(productMarketStats)
|
||||
.set({ isOutOfStock: nextIsOutOfStock })
|
||||
.where(inArray(productMarketStats.skuId, skus.map((sku) => sku.id)))
|
||||
|
||||
return { id: productId, isOutOfStock: nextIsOutOfStock }
|
||||
}
|
||||
|
||||
|
||||
// ==========================================================================
|
||||
// Product Helpers for Admin Controller
|
||||
|
|
|
|||
|
|
@ -429,6 +429,12 @@ export interface AdminDeleteProductResult {
|
|||
message: string;
|
||||
}
|
||||
|
||||
export interface AdminToggleOutOfStockResult {
|
||||
productId: number;
|
||||
isOutOfStock: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AdminUpdateSlotProductsResult {
|
||||
message: string;
|
||||
added: number;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue