This commit is contained in:
shafi54 2026-09-12 09:11:14 +05:30
parent f8e7eec754
commit ee60799a8b
8 changed files with 118 additions and 1 deletions

View file

@ -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>

View file

@ -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>

View file

@ -88,6 +88,7 @@ export {
updateProductGroup,
deleteProductGroup,
updateProductPrices,
toggleProductOutOfStock,
// Merge duplicate products into multi-SKU products
mergeDuplicateProducts,
// Admin - Slots

View file

@ -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'),

View file

@ -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.

View file

@ -99,6 +99,7 @@ export {
updateProductGroup,
deleteProductGroup,
updateProductPrices,
toggleProductOutOfStock,
} from './src/admin-apis/product'
export {

View file

@ -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

View file

@ -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;