DEAD_CODE_CLEAN #6

Merged
shafi merged 30 commits from DEAD_CODE_CLEAN into main 2026-09-14 04:29:36 +00:00
8 changed files with 118 additions and 1 deletions
Showing only changes of commit ee60799a8b - Show all commits

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 { AppContainer, MyText, tw, MyButton, useManualRefresh, SearchBar, useMarkDataFetchers, MyFlatList } from 'common-ui';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { SuccessToast, ErrorToast } from '@/services/toaster';
import type { AdminSku } from '@packages/shared'; import type { AdminSku } from '@packages/shared';
type FilterType = 'all' | 'in-stock' | 'out-of-stock'; 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 { 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); useManualRefresh(refetch);
useMarkDataFetchers(() => { useMarkDataFetchers(() => {
@ -61,6 +72,10 @@ export default function Products() {
router.push(`/(drawer)/dashboard/products/detail/${productId}` as any); 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 }) => ( const FilterButton = ({ filter, label, count }: { filter: FilterType; label: string; count: number }) => (
<TouchableOpacity <TouchableOpacity
onPress={() => setActiveFilter(filter)} onPress={() => setActiveFilter(filter)}
@ -193,6 +208,17 @@ export default function Products() {
<MaterialIcons name="edit" size={16} color="white" /> <MaterialIcons name="edit" size={16} color="white" />
<MyText style={tw`text-white font-semibold ml-1`}>Edit</MyText> <MyText style={tw`text-white font-semibold ml-1`}>Edit</MyText>
</TouchableOpacity> </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>
</View> </View>

View file

@ -1,9 +1,10 @@
import React, { useState, useMemo } from 'react' import React, { useState, useMemo } from 'react'
import { createFileRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router' 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 { AppContainer, p as MyText, MyButton, SearchBar, MyFlatList } from 'web-components'
import { trpc } from '@/lib/trpc-client' import { trpc } from '@/lib/trpc-client'
import { useManualRefresh, useMarkDataFetchers } from '@/lib/refresh-context' import { useManualRefresh, useMarkDataFetchers } from '@/lib/refresh-context'
import { SuccessToast, ErrorToast } from '@/services/toaster'
import type { AdminSku } from '@packages/shared' import type { AdminSku } from '@packages/shared'
export const Route = createFileRoute('/dashboard/products')({ export const Route = createFileRoute('/dashboard/products')({
@ -39,6 +40,16 @@ function Products() {
const { data: productsData, isLoading, error, refetch } = trpc.admin.product.getProducts.useQuery() 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) useManualRefresh(refetch)
useMarkDataFetchers(() => { useMarkDataFetchers(() => {
@ -70,6 +81,10 @@ function Products() {
navigate({ to: '/dashboard/products/$id', params: { id: String(productId) } }) 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. // Render child routes (new / edit / detail) when not on the list route.
// Placed after all hooks so the hook order stays stable across renders. // Placed after all hooks so the hook order stays stable across renders.
if (location.pathname !== '/dashboard/products') { if (location.pathname !== '/dashboard/products') {
@ -191,6 +206,17 @@ function Products() {
<Pencil size={16} color="white" /> <Pencil size={16} color="white" />
<MyText className="ml-1 font-semibold text-white">Edit</MyText> <MyText className="ml-1 font-semibold text-white">Edit</MyText>
</button> </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> </div>
</div> </div>

View file

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

View file

@ -17,6 +17,7 @@ import {
updateProductGroup as updateProductGroupInDb, updateProductGroup as updateProductGroupInDb,
deleteProductGroup as deleteProductGroupInDb, deleteProductGroup as deleteProductGroupInDb,
updateProductPrices as updateProductPricesInDb, updateProductPrices as updateProductPricesInDb,
toggleProductOutOfStock as toggleProductOutOfStockInDb,
checkProductExistsByName, checkProductExistsByName,
checkUnitExists, checkUnitExists,
createProduct as createProductInDb, createProduct as createProductInDb,
@ -46,6 +47,7 @@ import type {
AdminUpdateSlotProductsResult, AdminUpdateSlotProductsResult,
AdminSlotsProductIdsResult, AdminSlotsProductIdsResult,
AdminUpdateProductPricesResult, AdminUpdateProductPricesResult,
AdminToggleOutOfStockResult,
} from '@packages/shared' } 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 createProduct: protectedProcedure
.input(z.object({ .input(z.object({
name: z.string().min(1, 'Name is required'), 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. 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 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, updateProductGroup,
deleteProductGroup, deleteProductGroup,
updateProductPrices, updateProductPrices,
toggleProductOutOfStock,
} from './src/admin-apis/product' } from './src/admin-apis/product'
export { export {

View file

@ -1036,6 +1036,29 @@ export async function updateProductPrices(updates: Array<{
return { updatedCount: updates.length, invalidIds: [] } 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 // Product Helpers for Admin Controller

View file

@ -429,6 +429,12 @@ export interface AdminDeleteProductResult {
message: string; message: string;
} }
export interface AdminToggleOutOfStockResult {
productId: number;
isOutOfStock: boolean;
message: string;
}
export interface AdminUpdateSlotProductsResult { export interface AdminUpdateSlotProductsResult {
message: string; message: string;
added: number; added: number;