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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ packages/database/supabase/seed.sql
.env*.local

.react-router

packages/cad-rust
packages/cad-engine
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"deno.cacheOnSave": true,
"deno.enablePaths": ["./packages/database"],
"typescript.tsdk": "node_modules/typescript/lib",
"deno.enable": true
"deno.enable": true,
"git.ignoreLimitWarning": true
}
25 changes: 20 additions & 5 deletions apps/erp/app/components/Grid/components/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,26 @@ const Row = <T extends object>({
rowRef,
selectedCell,
onCellClick,
onCellUpdate
onCellUpdate,
onEditRow
}: RowProps<T>) => {
const onUpdate = onCellUpdate(row.index);

return (
<Tr
key={row.id}
ref={rowRef}
className={cn(rowIsClickable && "cursor-pointer")}
className={cn((rowIsClickable || onEditRow) && "cursor-pointer")}
onClick={
onEditRow
? (e: React.MouseEvent) => {
// Don't trigger row click if clicking a button/menu inside the row
const target = e.target as HTMLElement;
if (target.closest("button, a, [role='menuitem']")) return;
onEditRow(row.original);
}
: undefined
}
>
{row.getVisibleCells().map((cell, columnIndex) => {
const isSelected =
Expand All @@ -55,9 +66,13 @@ const Row = <T extends object>({
// @ts-ignore
editableComponents={editableComponents}
editedCells={editedCells}
isSelected={isSelected}
isEditing={isEditing}
onClick={() => onCellClick(cell.row.index, columnIndex)}
isSelected={onEditRow ? false : isSelected}
isEditing={onEditRow ? false : isEditing}
onClick={
onEditRow
? undefined
: () => onCellClick(cell.row.index, columnIndex)
}
onUpdate={onUpdate}
/>
);
Expand Down
5 changes: 4 additions & 1 deletion apps/erp/app/modules/items/items.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,10 @@ export const supplierPartValidator = z.object({
supplierUnitOfMeasureCode: zfd.text(z.string().optional()),
minimumOrderQuantity: zfd.numeric(z.number().min(0)),
conversionFactor: zfd.numeric(z.number().min(0)),
unitPrice: zfd.numeric(z.number().min(0))
unitPrice: zfd.numeric(z.number().min(0).optional()),
lastPurchaseDate: z.string().optional(),
lastPOQuantity: zfd.numeric(z.number().min(0).optional()),
lastPOId: z.string().optional()
});

export const toolValidator = itemValidator.merge(
Expand Down
93 changes: 90 additions & 3 deletions apps/erp/app/modules/items/ui/Item/SupplierPartForm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useCarbon } from "@carbon/auth";
import { ValidatedForm } from "@carbon/form";
import {
Button,
Expand Down Expand Up @@ -25,12 +26,15 @@ import {
Supplier,
UnitOfMeasure
} from "~/components/Form";
import { usePermissions, useUser } from "~/hooks";
import { useCurrencyFormatter, usePermissions, useUser } from "~/hooks";
import { path } from "~/utils/path";
import { supplierPartValidator } from "../../items.models";

type SupplierPartFormProps = {
initialValues: z.infer<typeof supplierPartValidator>;
initialValues: z.infer<typeof supplierPartValidator> & {
lastPurchaseDate?: string | null;
lastPOQuantity?: number | null;
};
type: "Part" | "Service" | "Tool" | "Consumable" | "Material";
unitOfMeasureCode: string;
onClose: () => void;
Expand All @@ -42,7 +46,9 @@ const SupplierPartForm = ({
unitOfMeasureCode,
onClose
}: SupplierPartFormProps) => {
const { carbon } = useCarbon();
const permissions = usePermissions();
const formatter = useCurrencyFormatter();

const { company } = useUser();
const baseCurrency = company?.baseCurrencyCode ?? "USD";
Expand All @@ -65,6 +71,29 @@ const SupplierPartForm = ({
const action = getAction(isEditing, type, itemId, initialValues.id);
const fetcher = useFetcher<{ success: boolean; message: string }>();

// Fetch price breaks for existing supplier parts
const [priceBreaks, setPriceBreaks] = useState<
{
quantity: number;
unitPrice: number;
leadTime: number | null;
sourceType: string;
}[]
>([]);

useEffect(() => {
if (!carbon || !isEditing || !initialValues.id) return;

carbon
.from("supplierPartPrice")
.select("quantity, unitPrice, leadTime, sourceType")
.eq("supplierPartId", initialValues.id)
.order("quantity", { ascending: true })
.then(({ data }) => {
if (data?.length) setPriceBreaks(data);
});
}, [carbon, isEditing, initialValues.id]);

useEffect(() => {
if (fetcher.data?.success) {
onClose();
Expand All @@ -80,7 +109,7 @@ const SupplierPartForm = ({
if (!open) onClose();
}}
>
<DrawerContent>
<DrawerContent size="lg">
<ValidatedForm
defaultValues={initialValues}
validator={supplierPartValidator}
Expand Down Expand Up @@ -110,6 +139,64 @@ const SupplierPartForm = ({
currency: baseCurrency
}}
/>
{/* Show last purchase info if available (read-only) */}
{initialValues.lastPurchaseDate && (
<div className="text-sm text-muted-foreground bg-muted/50 rounded-md p-3 space-y-1">
<div className="font-medium text-foreground">
Last Purchase Info
</div>
<div>
Date:{" "}
{new Date(
initialValues.lastPurchaseDate
).toLocaleDateString()}
</div>
{initialValues.lastPOQuantity != null && (
<div>
Quantity: {initialValues.lastPOQuantity.toLocaleString()}
</div>
)}
</div>
)}
{/* Show quantity price breaks if available */}
{priceBreaks.length > 0 && (
<div className="text-sm bg-muted/50 rounded-md p-3 space-y-2 w-full">
<div className="font-medium text-foreground">
Price Breaks
</div>
<table className="w-full text-sm">
<thead>
<tr className="text-muted-foreground border-b">
<th className="text-left py-1">Qty</th>
<th className="text-right py-1">Unit Price</th>
<th className="text-right py-1">Lead Time</th>
<th className="text-right py-1">Source</th>
</tr>
</thead>
<tbody>
{priceBreaks.map((pb) => (
<tr
key={pb.quantity}
className="border-b last:border-0"
>
<td className="py-1">
{pb.quantity.toLocaleString()}
</td>
<td className="text-right py-1">
{formatter.format(pb.unitPrice)}
</td>
<td className="text-right py-1">
{pb.leadTime != null ? `${pb.leadTime}d` : "—"}
</td>
<td className="text-right py-1 text-muted-foreground">
{pb.sourceType}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<UnitOfMeasure
name="supplierUnitOfMeasureCode"
label="Unit of Measure"
Expand Down
43 changes: 22 additions & 21 deletions apps/erp/app/modules/items/ui/Item/SupplierParts/SupplierParts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ import type { ColumnDef } from "@tanstack/react-table";
import { useMemo } from "react";
import { Outlet, useNavigate } from "react-router";
import { SupplierAvatar } from "~/components";
import {
EditableList,
EditableNumber,
EditableText
} from "~/components/Editable";
import { useUnitOfMeasure } from "~/components/Form/UnitOfMeasure";
import Grid from "~/components/Grid";
import { useCurrencyFormatter } from "~/hooks";
import { useCustomColumns } from "~/hooks/useCustomColumns";
Expand All @@ -25,6 +19,8 @@ type Part = Pick<
| "minimumOrderQuantity"
| "conversionFactor"
| "customFields"
| "lastPurchaseDate"
| "lastPOQuantity"
>;

type SupplierPartsProps = {
Expand All @@ -37,10 +33,9 @@ const SupplierParts = ({
compact = false
}: SupplierPartsProps) => {
const navigate = useNavigate();
const { canEdit, onCellEdit } = useSupplierParts();
const { canEdit } = useSupplierParts();

const formatter = useCurrencyFormatter();
const unitOfMeasureOptions = useUnitOfMeasure();
const customColumns = useCustomColumns<Part>("supplierPart");

const columns = useMemo<ColumnDef<Part>[]>(() => {
Expand All @@ -66,6 +61,24 @@ const SupplierParts = ({
renderTotal: true
}
},
{
accessorKey: "lastPurchaseDate",
header: "Last Purchase",
cell: (item) => {
const date = item.getValue<string>();
if (!date) return "—";
return new Date(date).toLocaleDateString();
}
},
{
accessorKey: "lastPOQuantity",
header: "Last PO Qty",
cell: (item) => {
const qty = item.getValue<number>();
if (qty == null) return "—";
return qty.toLocaleString();
}
},
{
accessorKey: "supplierUnitOfMeasureCode",
header: "Unit of Measure",
Expand All @@ -85,17 +98,6 @@ const SupplierParts = ({
return [...defaultColumns, ...customColumns];
}, [customColumns, formatter]);

const editableComponents = useMemo(
() => ({
supplierPartId: EditableText(onCellEdit),
supplierUnitOfMeasureCode: EditableList(onCellEdit, unitOfMeasureOptions),
minimumOrderQuantity: EditableNumber(onCellEdit),
conversionFactor: EditableNumber(onCellEdit),
unitPrice: EditableNumber(onCellEdit)
}),
[onCellEdit, unitOfMeasureOptions]
);

return (
<>
<Card className={cn(compact && "border-none p-0 dark:shadow-none")}>
Expand All @@ -107,8 +109,7 @@ const SupplierParts = ({
contained={false}
data={supplierParts}
columns={columns}
canEdit={canEdit}
editableComponents={editableComponents}
onEditRow={(row) => navigate(row.id!)}
onNewRow={canEdit ? () => navigate("new") : undefined}
/>
</CardContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,8 @@ const ComparisonView = ({
</span>
</HStack>
<span className="text-xs text-muted-foreground">
{linePrice.leadTime ?? 0} {pluralize(linePrice.leadTime ?? 0, "day")}
{linePrice.leadTime ?? 0}{" "}
{pluralize(linePrice.leadTime ?? 0, "day")}
</span>
</VStack>
</Td>
Expand Down Expand Up @@ -792,7 +793,10 @@ const LinePricingOptions = ({
<Td>{option.quantity}</Td>
<Td>{formatter.format(option.supplierUnitPrice ?? 0)}</Td>
<Td>{formatter.format(option.supplierShippingCost ?? 0)}</Td>
<Td>{option.leadTime ?? 0} {pluralize(option.leadTime ?? 0, "day")}</Td>
<Td>
{option.leadTime ?? 0}{" "}
{pluralize(option.leadTime ?? 0, "day")}
</Td>
<Td>{formatter.format(option.supplierTaxAmount ?? 0)}</Td>
<Td>
{formatter.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,9 @@ const LinePricingOptions = ({
option.supplierShippingCost ?? 0
)}
</Td>
<Td>{option.leadTime} {pluralize(option.leadTime, "day")}</Td>
<Td>
{option.leadTime} {pluralize(option.leadTime, "day")}
</Td>
<Td>
{presentationCurrencyFormatter.format(
option.supplierTaxAmount ?? 0
Expand Down
Loading