Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions apps/erp/app/modules/inventory/inventory.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,255 @@ export async function upsertWarehouseTransferLine(
}
}

export async function getShelvesWithItemCounts(
client: SupabaseClient<Database>,
locationId: string,
companyId: string,
args: GenericQueryFilters & {
search: string | null;
}
) {
// If searching, we need to find shelves that match by name OR contain matching items
let matchingShelfIds: string[] | null = null;

if (args?.search) {
// First, get shelf IDs that match by shelf name
const shelfNameMatches = await client
.from("shelf")
.select("id")
.eq("companyId", companyId)
.eq("locationId", locationId)
.eq("active", true)
.ilike("name", `%${args.search}%`);

const shelfNameMatchIds = new Set(
(shelfNameMatches.data ?? []).map((s) => s.id)
);

// Then, get shelf IDs that contain items matching the search
const itemMatches = await client
.from("itemLedger")
.select("shelfId, item!inner(name, readableIdWithRevision)")
.eq("companyId", companyId)
.eq("locationId", locationId)
.not("shelfId", "is", null)
.or(
`name.ilike.%${args.search}%,readableIdWithRevision.ilike.%${args.search}%`,
{ referencedTable: "item" }
);

const itemMatchShelfIds = new Set(
(itemMatches.data ?? [])
.map((l) => l.shelfId)
.filter((id): id is string => id !== null)
);

// Combine both sets
matchingShelfIds = [
...new Set([...shelfNameMatchIds, ...itemMatchShelfIds])
];

if (matchingShelfIds.length === 0) {
return { data: [], count: 0, error: null };
}
}

// Build the base query with pagination
let query = client
.from("shelf")
.select("id, name, locationId, active, createdAt, updatedAt", {
count: "exact"
})
.eq("companyId", companyId)
.eq("locationId", locationId)
.eq("active", true);

// If we have matching shelf IDs from search, filter by them
if (matchingShelfIds) {
query = query.in("id", matchingShelfIds);
}

// Apply generic query filters (pagination, sorting)
query = setGenericQueryFilters(query, args, [
{ column: "name", ascending: true }
]);

const shelves = await query;

if (shelves.error) {
return { data: [], count: 0, error: shelves.error };
}

const shelfIds = (shelves.data ?? []).map((s) => s.id);

if (shelfIds.length === 0) {
return { data: [], count: shelves.count ?? 0, error: null };
}

// Query itemLedger to get item counts per shelf for the paginated results
const itemLedgerQuery = client
.from("itemLedger")
.select("shelfId, itemId, quantity")
.in("shelfId", shelfIds)
.eq("companyId", companyId)
.eq("locationId", locationId);

const itemLedgers = await itemLedgerQuery;

// Aggregate quantities per shelf and count unique items
const shelfStats = new Map<
string,
{
itemCount: number;
totalQuantity: number;
items: Set<string>;
}
>();

if (itemLedgers.data) {
for (const ledger of itemLedgers.data) {
const shelfId = ledger.shelfId;
if (!shelfId) continue;

if (!shelfStats.has(shelfId)) {
shelfStats.set(shelfId, {
itemCount: 0,
totalQuantity: 0,
items: new Set()
});
}

const stats = shelfStats.get(shelfId)!;
if (!stats.items.has(ledger.itemId)) {
stats.items.add(ledger.itemId);
}
stats.totalQuantity += Number(ledger.quantity) || 0;
}

// Calculate final item counts
for (const stats of shelfStats.values()) {
stats.itemCount = stats.items.size;
}
}

// Merge shelf data with stats
const data = (shelves.data ?? []).map((shelf) => ({
...shelf,
itemCount: shelfStats.get(shelf.id)?.itemCount ?? 0,
totalQuantity: shelfStats.get(shelf.id)?.totalQuantity ?? 0
}));

return {
data,
count: shelves.count ?? 0,
error: null
};
}

export async function getShelfItems(
client: SupabaseClient<Database>,
shelfId: string,
locationId: string,
companyId: string,
args: GenericQueryFilters & {
search: string | null;
}
) {
// Get all item ledger entries for this shelf, grouped by item
const { data: ledgerData, error: ledgerError } = await client
.from("itemLedger")
.select(
`
itemId,
quantity,
item!inner(
id,
name,
readableIdWithRevision,
unitOfMeasureCode,
itemTrackingType
)
`
)
.eq("shelfId", shelfId)
.eq("locationId", locationId)
.eq("companyId", companyId);

if (ledgerError) {
return { data: [], count: 0, error: ledgerError };
}

// Aggregate quantities per item
const itemQuantities = new Map<
string,
{
itemId: string;
name: string;
readableIdWithRevision: string;
unitOfMeasureCode: string;
itemTrackingType: string;
quantity: number;
}
>();

for (const ledger of ledgerData ?? []) {
const item = ledger.item as {
id: string;
name: string;
readableIdWithRevision: string;
unitOfMeasureCode: string;
itemTrackingType: string;
};

if (!itemQuantities.has(ledger.itemId)) {
itemQuantities.set(ledger.itemId, {
itemId: ledger.itemId,
name: item.name,
readableIdWithRevision: item.readableIdWithRevision,
unitOfMeasureCode: item.unitOfMeasureCode,
itemTrackingType: item.itemTrackingType,
quantity: 0
});
}

const itemData = itemQuantities.get(ledger.itemId)!;
itemData.quantity += Number(ledger.quantity) || 0;
}

// Convert to array and filter items with quantity > 0
let items = Array.from(itemQuantities.values()).filter(
(item) => item.quantity > 0
);

// Apply search filter
if (args?.search) {
const searchLower = args.search.toLowerCase();
items = items.filter(
(item) =>
item.name.toLowerCase().includes(searchLower) ||
item.readableIdWithRevision?.toLowerCase().includes(searchLower)
);
}

// Sort by readable ID
items.sort((a, b) =>
(a.readableIdWithRevision ?? "").localeCompare(
b.readableIdWithRevision ?? ""
)
);

// Apply pagination
const offset = args.offset ?? 0;
const limit = args.limit ?? 50;
const paginatedItems = items.slice(offset, offset + limit);

return {
data: paginatedItems,
count: items.length,
error: null
};
}

export async function getDefaultShelfForJob(
client: SupabaseClient<Database>,
itemId: string,
Expand Down
24 changes: 20 additions & 4 deletions apps/erp/app/modules/inventory/ui/Inventory/InventoryShelves.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ import { nanoid } from "nanoid";
import { useMemo, useState } from "react";
import {
LuEllipsisVertical,
LuExternalLink,
LuPencil,
LuPrinter,
LuQrCode
} from "react-icons/lu";
import { Outlet } from "react-router";
import { Link, Outlet, useSearchParams } from "react-router";
import type { z } from "zod";
import { Enumerable } from "~/components/Enumerable";
import { Input, Location, Select, Shelf } from "~/components/Form";
Expand Down Expand Up @@ -74,6 +75,8 @@ const InventoryShelves = ({
}: InventoryShelvesProps) => {
const permissions = usePermissions();
const adjustmentModal = useDisclosure();
const [searchParams] = useSearchParams();
const locationId = searchParams.get("location");

const unitOfMeasures = useUnitOfMeasure();

Expand Down Expand Up @@ -169,10 +172,23 @@ const InventoryShelves = ({
{itemShelfQuantities
.filter((item) => item.quantity !== 0)
.map((item, index) => (
<Tr key={index}>
<Tr key={index} className="group">
<Td>
{shelves.find((s) => s.value === item.shelfId)?.label ||
item.shelfId}
<div className="flex items-center gap-2">
<span>
{shelves.find((s) => s.value === item.shelfId)
?.label || item.shelfId}
</span>
{item.shelfId && (
<Link
to={`${path.to.shelfInventory(item.shelfId)}${locationId ? `?location=${locationId}` : ""}`}
className="text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100 transition-opacity duration-200"
title="View shelf"
>
<LuExternalLink className="h-4 w-4" />
</Link>
)}
</div>
</Td>

<Td>
Expand Down
Loading