- Add thermal/full-page receipt format toggle (per-device, localStorage) - Full-page receipt uses clean invoice layout matching repair PDF style - Settings page reorganized into tabbed sections (Store, Locations, Modules, Receipt, POS Security, Advanced) - Manager override system: configurable PIN prompt for void, refund, discount, cash in/out - Discount threshold setting: require manager approval above X% - Consumable product type: tracked for internal job costing, excluded from POS search, receipts, and customer-facing totals - Repair line item dialog: product picker dropdown for parts/consumables from inventory - Repair → POS checkout: load ready-for-pickup tickets into repair_payment transactions with proper tax categories (labor=service, parts=goods) - Transaction completion auto-updates repair ticket status to picked_up - POS Repairs dialog with Pickup and New Intake tabs, customer account lookup - Inline price adjustment on cart items: % off, $ off, or set price with live preview - Order-level discount button with same three input modes - Backend: migration 0043 (consumable enum + is_consumable flag), createFromRepairTicket service, ready-for-pickup endpoint - Fix: backend dev script uses --env-file for turbo compatibility Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
/** Coerce empty strings to undefined — solves HTML form inputs sending '' for blank optional fields */
|
|
function opt<T extends z.ZodTypeAny>(schema: T) {
|
|
return z.preprocess((v) => (v === '' ? undefined : v), schema.optional())
|
|
}
|
|
|
|
export const CategoryCreateSchema = z.object({
|
|
name: z.string().min(1).max(255),
|
|
description: opt(z.string()),
|
|
parentId: opt(z.string().uuid()),
|
|
sortOrder: z.number().int().default(0),
|
|
})
|
|
export type CategoryCreateInput = z.infer<typeof CategoryCreateSchema>
|
|
|
|
export const CategoryUpdateSchema = CategoryCreateSchema.partial()
|
|
export type CategoryUpdateInput = z.infer<typeof CategoryUpdateSchema>
|
|
|
|
export const SupplierCreateSchema = z.object({
|
|
name: z.string().min(1).max(255),
|
|
contactName: opt(z.string().max(255)),
|
|
email: opt(z.string().email()),
|
|
phone: opt(z.string().max(50)),
|
|
website: opt(z.string().max(255)),
|
|
accountNumber: opt(z.string().max(100)),
|
|
paymentTerms: opt(z.string().max(100)),
|
|
notes: opt(z.string()),
|
|
})
|
|
export type SupplierCreateInput = z.infer<typeof SupplierCreateSchema>
|
|
|
|
export const SupplierUpdateSchema = SupplierCreateSchema.partial()
|
|
export type SupplierUpdateInput = z.infer<typeof SupplierUpdateSchema>
|
|
|
|
// System slugs — used for code-level business logic references.
|
|
// Actual valid values are stored in lookup tables and are company-configurable.
|
|
export const SystemItemCondition = z.enum(['new', 'excellent', 'good', 'fair', 'poor'])
|
|
export const SystemUnitStatus = z.enum([
|
|
'available',
|
|
'sold',
|
|
'rented',
|
|
'on_trial',
|
|
'in_repair',
|
|
'layaway',
|
|
'lost',
|
|
'retired',
|
|
])
|
|
|
|
// API validation accepts any string slug (validated against lookup table at service layer)
|
|
export const ItemCondition = z.string().min(1).max(100)
|
|
export const UnitStatus = z.string().min(1).max(100)
|
|
|
|
export const ProductCreateSchema = z.object({
|
|
sku: opt(z.string().max(100)),
|
|
upc: opt(z.string().max(100)),
|
|
name: z.string().min(1).max(255),
|
|
description: opt(z.string()),
|
|
brand: opt(z.string().max(255)),
|
|
model: opt(z.string().max(255)),
|
|
categoryId: opt(z.string().uuid()),
|
|
locationId: opt(z.string().uuid()),
|
|
isSerialized: z.boolean().default(false),
|
|
isRental: z.boolean().default(false),
|
|
isDualUseRepair: z.boolean().default(false),
|
|
isConsumable: z.boolean().default(false),
|
|
price: z.number().min(0).optional(),
|
|
minPrice: z.number().min(0).optional(),
|
|
rentalRateMonthly: z.number().min(0).optional(),
|
|
qtyOnHand: z.number().int().min(0).default(0),
|
|
qtyReorderPoint: z.number().int().min(0).optional(),
|
|
})
|
|
export type ProductCreateInput = z.infer<typeof ProductCreateSchema>
|
|
|
|
export const ProductUpdateSchema = ProductCreateSchema.partial()
|
|
export type ProductUpdateInput = z.infer<typeof ProductUpdateSchema>
|
|
|
|
export const ProductSearchSchema = z.object({
|
|
q: z.string().min(1).max(255),
|
|
})
|
|
|
|
export const InventoryUnitCreateSchema = z.object({
|
|
productId: z.string().uuid(),
|
|
locationId: opt(z.string().uuid()),
|
|
serialNumber: opt(z.string().max(255)),
|
|
condition: ItemCondition.default('new'),
|
|
status: UnitStatus.default('available'),
|
|
purchaseDate: opt(z.string().date()),
|
|
purchaseCost: z.number().min(0).optional(),
|
|
notes: opt(z.string()),
|
|
})
|
|
export type InventoryUnitCreateInput = z.infer<typeof InventoryUnitCreateSchema>
|
|
|
|
export const InventoryUnitUpdateSchema = InventoryUnitCreateSchema.omit({ productId: true }).partial()
|
|
export type InventoryUnitUpdateInput = z.infer<typeof InventoryUnitUpdateSchema>
|
|
|
|
export const ProductSupplierCreateSchema = z.object({
|
|
supplierId: z.string().uuid(),
|
|
supplierSku: opt(z.string().max(100)),
|
|
isPreferred: z.boolean().default(false),
|
|
})
|
|
export type ProductSupplierCreateInput = z.infer<typeof ProductSupplierCreateSchema>
|
|
|
|
export const ProductSupplierUpdateSchema = ProductSupplierCreateSchema.omit({ supplierId: true }).partial()
|
|
export type ProductSupplierUpdateInput = z.infer<typeof ProductSupplierUpdateSchema>
|
|
|
|
export const StockReceiptCreateSchema = z.object({
|
|
supplierId: opt(z.string().uuid()),
|
|
inventoryUnitId: opt(z.string().uuid()),
|
|
qty: z.number().int().min(1).default(1),
|
|
costPerUnit: z.number().min(0),
|
|
receivedDate: z.string().date(),
|
|
invoiceNumber: opt(z.string().max(100)),
|
|
notes: opt(z.string()),
|
|
})
|
|
export type StockReceiptCreateInput = z.infer<typeof StockReceiptCreateSchema>
|