diff --git a/app/blocks/sales-log/delete-sale-modal.tsx b/app/blocks/sales-log/delete-sale-modal.tsx new file mode 100644 index 0000000..6e0e59d --- /dev/null +++ b/app/blocks/sales-log/delete-sale-modal.tsx @@ -0,0 +1,57 @@ +import { Form } from "react-router"; +import { IconX } from "@tabler/icons-react"; +import styles from "./log-sale-modal.module.css"; + +interface Props { + saleId: string; + onClose: () => void; +} + +export function DeleteSaleModal({ saleId, onClose }: Props) { + return ( +
e.target === e.currentTarget && onClose()} + > +
+
+ Delete Sale + + +
+ +
+ + + +
+ Are you sure you want to delete this sale? +
+ +
+ + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/blocks/sales-log/log-sale-modal.tsx b/app/blocks/sales-log/log-sale-modal.tsx index de2bde3..5e231a1 100644 --- a/app/blocks/sales-log/log-sale-modal.tsx +++ b/app/blocks/sales-log/log-sale-modal.tsx @@ -3,99 +3,255 @@ import { Form } from "react-router"; import { IconX } from "@tabler/icons-react"; import styles from "./log-sale-modal.module.css"; -interface Props { className?: string; onClose: () => void; } - -export function LogSaleModal({ className, onClose }: Props) { - const [salePrice, setSalePrice] = useState(""); - const [platformFee, setPlatformFee] = useState(""); - const [shippingCost, setShippingCost] = useState(""); - const [selectedItemId, setSelectedItemId] = useState(""); - const [selectedItem, setSelectedItem] = useState(null); +interface Props { + className?: string; + onClose: () => void; + inventory?: any[]; + sale?: any; +} + +export function LogSaleModal({ + className, + onClose, + inventory = [], + sale, +}: Props) { + const [salePrice, setSalePrice] = useState( + sale ? sale.salePrice.toString() : "" + ); + + const [platformFee, setPlatformFee] = useState( + sale ? (sale.platformFee ?? "").toString() : "" + ); + + const [shippingCost, setShippingCost] = useState( + sale ? (sale.shippingCost ?? "").toString() : "" + ); + + const [selectedItemId, setSelectedItemId] = useState( + sale ? sale.inventoryItemId : "" + ); + + const [selectedItem, setSelectedItem] = useState( + sale ? sale.inventoryItem : null + ); + const [search, setSearch] = useState(""); const [searchResults, setSearchResults] = useState([]); - + + const [saleDate, setSaleDate] = useState( + sale + ? new Date(sale.saleDate).toISOString().split("T")[0] + : "" + ); + + useEffect(() => { + if (sale) { + setSelectedItemId(sale.inventoryItemId); + setSelectedItem(sale.inventoryItem); + setSalePrice(sale.salePrice.toString()); + setPlatformFee((sale.platformFee ?? "").toString()); + setShippingCost((sale.shippingCost ?? "").toString()); + setSaleDate(new Date(sale.saleDate).toISOString().split("T")[0]); + } + }, [sale]); + useEffect(() => { if (search.length < 2) { setSearchResults([]); return; } + const delayDebounceFn = setTimeout(() => { fetch(`/api/inventory/search?q=${search}`) - .then(res => res.json()) - .then(data => setSearchResults(data.items || [])) - .catch(err => console.error(err)); + .then((res) => res.json()) + .then((data) => setSearchResults(data.items || [])) + .catch(console.error); }, 300); return () => clearTimeout(delayDebounceFn); }, [search]); - const purchasePrice = selectedItem ? Number(selectedItem.purchasePrice) : 0; + const currentItem = sale + ? sale.inventoryItem + : selectedItem || inventory.find((i) => i.id === selectedItemId); + + const purchasePrice = parseFloat( + String(currentItem?.purchasePrice ?? "0") + ); + + const salePriceValue = parseFloat(salePrice || "0"); const platformFeeValue = parseFloat(platformFee || "0"); const shippingCostValue = parseFloat(shippingCost || "0"); - const profit = salePrice && selectedItem ? parseFloat(salePrice) - purchasePrice - platformFeeValue - shippingCostValue : null; + + const profit = + currentItem + ? salePriceValue - + purchasePrice - + platformFeeValue - + shippingCostValue + : null; return (
e.target === e.currentTarget && onClose()}>
- Log Sale + {sale ? "Edit Sale" : "Log Sale"}
-
onClose()}> - + + {sale && ( + )}
-
- - { setSearch(e.target.value); setSelectedItem(null); setSelectedItemId(""); }} - required={!selectedItemId} - /> - {searchResults.length > 0 && !selectedItem && ( -
    - {searchResults.map(item => ( -
  • { - setSelectedItem(item); - setSelectedItemId(item.id); - setSearchResults([]); - setSearch(`${item.name} (${item.size}) - $${Number(item.purchasePrice)}`); +
    + + + {sale ? ( + + ) : ( + <> + { + setSearch(e.target.value); + setSelectedItem(null); + setSelectedItemId(""); + }} + required={!selectedItemId} + /> + + {searchResults.length > 0 && !selectedItem && ( +
      - {item.name} ({item.size}) - ${Number(item.purchasePrice)} - - ))} -
    + {searchResults.map((item) => ( +
  • { + setSelectedItem(item); + setSelectedItemId(item.id); + setSearchResults([]); + setSearch( + `${item.name} (${item.size}) - $${Number( + item.purchasePrice + )}` + ); + }} + > + {item.name} ({item.size}) - ${Number(item.purchasePrice)} +
  • + ))} +
+ )} + + + )} -
+
- setSalePrice(e.target.value)} /> + setSalePrice(e.target.value)} + />
+
- + setSaleDate(e.target.value)} + />
+
- setPlatformFee(e.target.value)} /> + setPlatformFee(e.target.value)} + />
+
- setShippingCost(e.target.value)} /> + setShippingCost(e.target.value)} + />
- @@ -115,7 +271,12 @@ export function LogSaleModal({ className, onClose }: Props) {
- +
{profit !== null && ( @@ -127,7 +288,13 @@ export function LogSaleModal({ className, onClose }: Props) {
- +
diff --git a/app/blocks/sales-log/sales-table.module.css b/app/blocks/sales-log/sales-table.module.css index aba77eb..d9e471e 100644 --- a/app/blocks/sales-log/sales-table.module.css +++ b/app/blocks/sales-log/sales-table.module.css @@ -8,3 +8,26 @@ .profitBadge { display: inline-flex; padding: 3px 8px; border-radius: var(--radius-full); font-size: 12px; font-weight: 700; font-family: var(--family-mono); } .positive { background: var(--color-success-bg); color: var(--color-success); } .negative { background: var(--color-danger-bg); color: var(--color-danger); } +.editBtn, +.deleteBtn { + border: none; + background: transparent; + cursor: pointer; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.editBtn { + color: #16a34a; + margin-right: 10px; +} + +.deleteBtn { + color: #2563eb; +} +.actionButtons { + display: flex; + gap: 8px; +} \ No newline at end of file diff --git a/app/blocks/sales-log/sales-table.tsx b/app/blocks/sales-log/sales-table.tsx index a9b4deb..aea6963 100644 --- a/app/blocks/sales-log/sales-table.tsx +++ b/app/blocks/sales-log/sales-table.tsx @@ -1,8 +1,20 @@ import styles from "./sales-table.module.css"; +import { IconEdit, IconTrash } from "@tabler/icons-react"; -interface Props { className?: string; sales?: any[]; } +interface Props { + className?: string; + sales?: any[]; + onEdit?: (sale: any) => void; + onDelete?: (sale: any) => void; +} -export function SalesTable({ className, sales = [] }: Props) { +export function SalesTable({ + className, + sales = [], + onEdit, + onDelete, +}: Props) { + return (
@@ -15,33 +27,64 @@ export function SalesTable({ className, sales = [] }: Props) { Date Margin Profit + Actions - {sales.map(s => { - const salePrice = Number(s.salePrice); - const cost = Number(s.inventoryItem.purchasePrice); - const platformFee = Number(s.platformFee); - const shippingCost = Number(s.shippingCost); - const profit = salePrice - cost - platformFee - shippingCost; - const margin = salePrice > 0 ? ((profit / salePrice) * 100).toFixed(1) : 0; - const dateObj = new Date(s.saleDate); - - return ( - - {s.inventoryItem.name} - {s.marketplace} - ${salePrice.toFixed(2)} - {dateObj.toLocaleDateString()} - {margin}% - - = 0 ? styles.positive : styles.negative].join(" ")}> - {profit >= 0 ? "+" : ""}${profit.toFixed(2)} - - - - ); - })} + {sales.map((s) => { + const salePrice = Number(s.salePrice); + const cost = Number(s.inventoryItem.purchasePrice); + const platformFee = Number(s.platformFee ?? 0); + const shippingCost = Number(s.shippingCost ?? 0); + + const profit = salePrice - cost - platformFee - shippingCost; + const margin = + salePrice > 0 ? ((profit / salePrice) * 100).toFixed(1) : "0.0"; + + const dateObj = new Date(s.saleDate); + + return ( + + {s.inventoryItem.name} + {s.marketplace} + ${salePrice.toFixed(2)} + {dateObj.toLocaleDateString()} + {margin}% + + + = 0 ? styles.positive : styles.negative, + ].join(" ")} + > + {profit >= 0 ? "+" : ""} + ${profit.toFixed(2)} + + + + + + + + + + ); +})}
diff --git a/app/routes/sales-log.tsx b/app/routes/sales-log.tsx index af1c77a..b67a800 100644 --- a/app/routes/sales-log.tsx +++ b/app/routes/sales-log.tsx @@ -9,6 +9,7 @@ import { SalesHeader } from "~/blocks/sales-log/sales-header"; import { SalesSummaryCards } from "~/blocks/sales-log/sales-summary-cards"; import { SalesTable } from "~/blocks/sales-log/sales-table"; import { LogSaleModal } from "~/blocks/sales-log/log-sale-modal"; +import { DeleteSaleModal } from "~/blocks/sales-log/delete-sale-modal"; import { Pagination } from "~/blocks/__global/pagination"; import { CACHE_PRIVATE_NO_STORE } from "~/utils/cache-headers"; @@ -37,7 +38,7 @@ export async function loader({ request }: Route.LoaderArgs) { const page = Math.max(1, Number(url.searchParams.get("page")) || 1); const pageSize = Number(url.searchParams.get("pageSize")) || 10; - const [totalSales, sales, metricsResult] = await Promise.all([ + const [totalSales, sales, inventory, metricsResult] = await Promise.all([ prisma.sale.count({ where: { userId: user.id } }), prisma.sale.findMany({ where: { userId: user.id }, @@ -46,7 +47,16 @@ export async function loader({ request }: Route.LoaderArgs) { skip: (page - 1) * pageSize, take: pageSize, }), - + prisma.inventoryItem.findMany({ + where: { + userId: user.id, + status: "IN_STOCK", + }, + orderBy: { + createdAt: "desc", + }, +}), + prisma.$queryRaw<{ totalRevenue: number; totalProfit: number }[]>` SELECT COALESCE(SUM(s."salePrice"), 0) AS "totalRevenue", @@ -66,25 +76,32 @@ export async function loader({ request }: Route.LoaderArgs) { ` ]); - const totalRevenue = Number(metricsResult[0]?.totalRevenue || 0); - const totalProfit = Number(metricsResult[0]?.totalProfit || 0); +const totalRevenue = Number(metricsResult[0]?.totalRevenue || 0); +const totalProfit = Number(metricsResult[0]?.totalProfit || 0); - return { - sales: sales.map(s => ({ - ...s, - salePrice: Number(s.salePrice), - inventoryItem: { - ...s.inventoryItem, - purchasePrice: Number(s.inventoryItem.purchasePrice), - } - })), - totalPages: Math.ceil(totalSales / pageSize), - summary: { - totalSalesCount: totalSales, - totalRevenue, - totalProfit, + + return { + sales: sales.map((sale) => ({ + ...sale, + salePrice: Number(sale.salePrice), + platformFee: Number(sale.platformFee), + shippingCost: Number(sale.shippingCost), + + inventoryItem: { + ...sale.inventoryItem, + purchasePrice: Number(sale.inventoryItem.purchasePrice), }, - }; + })), + + inventory, + totalPages: Math.ceil(totalSales / pageSize), + + summary: { + totalSalesCount: totalSales, + totalRevenue, + totalProfit, + }, +}; } export async function action({ request }: Route.ActionArgs) { @@ -101,8 +118,8 @@ export async function action({ request }: Route.ActionArgs) { const inventoryItemId = formData.get("inventoryItemId") as string; const salePrice = Number(formData.get("salePrice")); - const platformFee = Number(formData.get("platformFee") || 0); - const shippingCost = Number(formData.get("shippingCost") || 0); + const platformFee = Number(formData.get("platformFee") || 0); + const shippingCost = Number(formData.get("shippingCost") || 0); const saleDate = new Date(formData.get("saleDate") as string); const marketplace = formData.get("marketplace") as any; @@ -144,31 +161,144 @@ export async function action({ request }: Route.ActionArgs) { }), ]); } + if (intent === "edit") { + const saleId = formData.get("saleId") as string; + const salePrice = Number(formData.get("salePrice")); + const saleDate = new Date(formData.get("saleDate") as string); + const marketplace = formData.get("marketplace") as any; + const trackingNumber = formData.get("trackingNumber") as string; + const platformFee = Number(formData.get("platformFee") || 0); +const shippingCost = Number(formData.get("shippingCost") || 0); + + const sale = await prisma.sale.findFirst({ + where: { + id: saleId, + userId: user.id, + }, + }); + + if (!sale) { + return { ok: false }; + } + + await prisma.sale.update({ + where: { + id: saleId, + }, + data: { + platformFee, + shippingCost, + salePrice, + saleDate, + marketplace, + trackingNumber, + }, + }); + + return { ok: true, intent: "edit" }; +} + + if (intent === "delete") { + const saleId = formData.get("saleId") as string; + + const sale = await prisma.sale.findFirst({ + where: { id: saleId, + userId: user.id, + }, + }); + + if (!sale) { + return { ok: false }; + } + + await prisma.$transaction([ + prisma.sale.delete({ + where: { id: saleId }, + }), + prisma.inventoryItem.update({ + where: { id: sale.inventoryItemId }, + data: { + status: "IN_STOCK", + }, + }), + ]); + return { ok: true, intent: "delete" }; + } return { ok: true, intent }; } export default function SalesLogPage() { - const { sales, totalPages, summary } = useLoaderData(); + const { sales,inventory, totalPages, summary } = useLoaderData(); const actionData = useActionData(); const [showLogSale, setShowLogSale] = useState(false); + const [selectedSale, setSelectedSale] = useState(null); + const [showDelete, setShowDelete] = useState(false); + const [showEdit, setShowEdit] = useState(false); - useEffect(() => { - if (actionData?.ok) { - if (actionData.intent === "create") { - toast.success("Sale logged successfully"); - setShowLogSale(false); - } - } - }, [actionData]); - - return ( -
- setShowLogSale(true)} /> - - - - {showLogSale && setShowLogSale(false)} />} -
- ); +useEffect(() => { + if (!actionData?.ok) return; + + if (actionData.intent === "create") { + toast.success("Sale logged successfully"); + setShowLogSale(false); + } + + if (actionData.intent === "edit") { + toast.success("Sale updated successfully"); + setShowEdit(false); + } + + if (actionData.intent === "delete") { + toast.success("Sale deleted successfully"); + setShowDelete(false); + } +}, [actionData]); + +return ( +
+ setShowLogSale(true)} /> + + + + { + setSelectedSale(sale); + setShowEdit(true); + }} + onDelete={(sale) => { + setSelectedSale(sale); + setShowDelete(true); + }} + /> + + + + {showLogSale && ( + setShowLogSale(false)} + /> + )} + + {showEdit && ( + setShowEdit(false)} + /> + )} + + {showDelete && selectedSale && ( + setShowDelete(false)} + /> + )} +
+); }