132 lines
No EOL
4.4 KiB
TypeScript
132 lines
No EOL
4.4 KiB
TypeScript
/**
|
|
* Stock Quick-Draft service — backs the /stock-orders modal that lets
|
|
* a buyer add items to draft stock_purchases in an item-first rhythm.
|
|
*
|
|
* Reuses `generateStockPurchaseNumber` from `stock-purchases-service.ts`
|
|
* for the find-or-create open-draft step; no schema changes, no new
|
|
* statuses. Admin PB only (consistent with the other service modules).
|
|
*/
|
|
import type PocketBase from "pocketbase";
|
|
import { getAdminPocketBase } from "./pocketbase-admin";
|
|
import { generateStockPurchaseNumber } from "./stock-purchases-service";
|
|
|
|
const STOCK_ITEMS = "stock_items";
|
|
const STOCK_PURCHASES = "stock_purchases";
|
|
const STOCK_PURCHASE_ITEMS = "stock_purchase_items";
|
|
|
|
const SEARCH_LIMIT_MAX = 25;
|
|
const SEARCH_LIMIT_DEFAULT = 10;
|
|
|
|
/* ============================================================================
|
|
* Minimal PB client shape this module needs.
|
|
*
|
|
* The route passes in the real `getAdminPocketBase()`; the unit test
|
|
* passes in a tiny mock. Only the methods we actually call are typed.
|
|
* ========================================================================== */
|
|
|
|
export interface StockQuickDraftPb {
|
|
collection: (name: string) => {
|
|
getList: (
|
|
page?: number,
|
|
perPage?: number,
|
|
opts?: { filter?: string; sort?: string; expand?: string }
|
|
) => Promise<{
|
|
items: Array<Record<string, unknown>>;
|
|
totalItems: number;
|
|
page: number;
|
|
perPage: number;
|
|
totalPages: number;
|
|
}>;
|
|
getFullList: (opts?: { filter?: string }) => Promise<Array<Record<string, unknown>>>;
|
|
getOne: (id: string, opts?: { expand?: string }) => Promise<Record<string, unknown>>;
|
|
create: (data: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
update: (id: string, data: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
delete: (id: string) => Promise<Record<string, unknown>>;
|
|
};
|
|
}
|
|
|
|
/* ============================================================================
|
|
* Public types
|
|
* ========================================================================== */
|
|
|
|
export interface StockItemSearchResult {
|
|
id: string;
|
|
sku: string;
|
|
name: string;
|
|
description: string;
|
|
unit?: string;
|
|
}
|
|
|
|
export interface SupplierHistoryEntry {
|
|
supplierId: string;
|
|
supplierName: string;
|
|
lastCost: number;
|
|
lastDate: string;
|
|
timesBought: number;
|
|
}
|
|
|
|
export interface AddQuickDraftLineNewStockItem {
|
|
name: string;
|
|
sku: string;
|
|
description?: string;
|
|
unit?: string;
|
|
}
|
|
|
|
export interface AddQuickDraftLineInput {
|
|
supplierId: string;
|
|
stockItemId?: string;
|
|
quantity: number;
|
|
costPrice: number;
|
|
newStockItem?: AddQuickDraftLineNewStockItem;
|
|
}
|
|
|
|
export type AddQuickDraftLineResult =
|
|
| { ok: true; purchaseId: string; purchaseNumber: string; lineId: string; createdDraft: boolean }
|
|
| { ok: false; reason: string; message: string };
|
|
|
|
export type RemoveQuickDraftLineResult =
|
|
| { ok: true }
|
|
| { ok: false; reason: string; message: string };
|
|
|
|
/* ============================================================================
|
|
* Helpers
|
|
* ========================================================================== */
|
|
|
|
async function resolvePb(pb?: StockQuickDraftPb): Promise<StockQuickDraftPb> {
|
|
if (pb) return pb;
|
|
return (await getAdminPocketBase()) as unknown as StockQuickDraftPb;
|
|
}
|
|
|
|
/* ============================================================================
|
|
* searchStockItemsForQuickDraft
|
|
* ========================================================================== */
|
|
|
|
/**
|
|
* Typeahead search over `stock_items`. Matches `sku` OR `name` with
|
|
* PocketBase's `~` (contains, case-insensitive). Excludes inactive
|
|
* items. Limit defaults to 10, clamped to a max of 25.
|
|
*
|
|
* Returns a narrow projection — only the fields the modal needs.
|
|
*/
|
|
export async function searchStockItemsForQuickDraft(
|
|
pb: StockQuickDraftPb,
|
|
q: string,
|
|
limit?: number,
|
|
): Promise<StockItemSearchResult[]> {
|
|
const safeLimit = Math.min(Math.max(limit ?? SEARCH_LIMIT_DEFAULT, 1), SEARCH_LIMIT_MAX);
|
|
const escaped = q.replace(/"/g, '\\"');
|
|
const filter = `is_active=true && (sku ~ "${escaped}" || name ~ "${escaped}")`;
|
|
|
|
const result = await pb.collection(STOCK_ITEMS).getList(1, safeLimit, {
|
|
filter,
|
|
sort: "name",
|
|
});
|
|
|
|
return result.items.map((row) => ({
|
|
id: String(row.id),
|
|
sku: String(row.sku ?? ""),
|
|
name: String(row.name ?? ""),
|
|
description: String(row.description ?? ""),
|
|
unit: row.unit ? String(row.unit) : undefined,
|
|
}));
|
|
} |