/ packages / frontend / src / components / merchants / Transactions.tsx
Transactions.tsx
  1  import { useEffect, useState } from "react";
  2  import { Link } from "@tanstack/react-router";
  3  import { formatUnits } from "viem";
  4  
  5  import { Order } from "@massmarket/schema";
  6  import { CodecValue } from "@massmarket/utils/codec";
  7  
  8  import { OrderId, OrderState } from "../../types.ts";
  9  import { useStateManager } from "../../hooks/useStateManager.ts";
 10  import { useBaseToken } from "../../hooks/useBaseToken.ts";
 11  import { formatDate, OrderStateFromNumber } from "../../utils/helper.ts";
 12  
 13  export default function Transactions(
 14    { displayFive }: { displayFive?: boolean },
 15  ) {
 16    const { stateManager } = useStateManager();
 17    const { baseToken } = useBaseToken();
 18  
 19    const [filter, setFilter] = useState<string>("all");
 20    const [sort, setSort] = useState<string>("oldest");
 21    const [orders, setOrders] = useState<Map<OrderId, Order>>(new Map());
 22  
 23    function mapToOrderClass(orders: CodecValue) {
 24      if (!(orders instanceof Map)) {
 25        throw new Error("Orders is not a Map");
 26      }
 27      const allOrders = new Map();
 28      for (
 29        const [id, o] of orders.entries()
 30      ) {
 31        allOrders.set(id, Order.fromCBOR(o));
 32      }
 33      return allOrders;
 34    }
 35  
 36    useEffect(() => {
 37      if (!stateManager) return;
 38  
 39      function ordersEvent(res: CodecValue) {
 40        const allOrders = mapToOrderClass(res);
 41        setOrders(allOrders);
 42      }
 43  
 44      stateManager.get(["Orders"]).then(
 45        (res: CodecValue | undefined) => {
 46          if (!res) return;
 47          const allOrders = mapToOrderClass(res);
 48          setOrders(allOrders);
 49        },
 50      );
 51  
 52      stateManager.events.on(ordersEvent, ["Orders"]);
 53  
 54      return () => {
 55        stateManager.events.off(
 56          ordersEvent,
 57          ["Orders"],
 58        );
 59      };
 60    }, [stateManager]);
 61  
 62    function renderTransactions() {
 63      if (!orders.size) {
 64        return (
 65          <div data-testid="no-transactions">
 66            <p>No transactions</p>
 67          </div>
 68        );
 69      }
 70      const transactions = Array.from([...orders.entries()])
 71        .filter(([_, value]) => {
 72          if (filter === "paid") {
 73            return value.State === OrderState.Paid;
 74          }
 75          return true;
 76        }).sort((a, b) => {
 77          if (!a[1].PaymentDetails) return 1;
 78          if (!b[1].PaymentDetails) return -1;
 79  
 80          if (sort === "oldest") {
 81            return a[1].PaymentDetails.TTL - b[1].PaymentDetails.TTL;
 82          }
 83          return b[1].PaymentDetails.TTL - a[1].PaymentDetails.TTL;
 84        });
 85  
 86      const displayTransactions = displayFive
 87        ? transactions.slice(0, 5)
 88        : transactions;
 89  
 90      return displayTransactions.map(([key, value]) => {
 91        const ID = key;
 92        let date = "-";
 93        let time = "-";
 94        let total = "-";
 95        if (value.PaymentDetails) {
 96          const d = formatDate(value.PaymentDetails!.TTL).split(",");
 97          date = d[0];
 98          time = d[1];
 99  
100          total = `${
101            formatUnits(BigInt(value.PaymentDetails.Total), baseToken.decimals)
102          } ${baseToken.symbol}`;
103        }
104  
105        return (
106          <Link
107            data-testid="transaction"
108            key={ID}
109            to="/order-details"
110            search={(prev: Record<string, string>) => ({
111              shopId: prev.shopId,
112              orderId: ID,
113            })}
114            style={{ color: "black" }}
115          >
116            <div className=" p-3 grid grid-cols-5 text-center bg-white">
117              <p data-testid={ID} className="truncate">
118                {ID.toString().slice(0, 8)}...
119              </p>
120              <p className="truncate">{date}</p>
121              <p className="truncate">{time}</p>
122              <p className="truncate">{total}</p>
123              <p data-testid="status" className="truncate">
124                {OrderStateFromNumber(value.State)}
125              </p>
126            </div>
127          </Link>
128        );
129      });
130    }
131    return (
132      <section className="transactions-container">
133        <section className="flex items-center gap-4">
134          <div className="flex items-center gap-1">
135            <p>Filter:</p>
136            <select
137              name="filter"
138              id="filter"
139              className="cursor-pointer"
140              value={filter}
141              onChange={(e) => setFilter(e.target.value)}
142            >
143              <option value="all">All</option>
144              <option value="paid">Paid</option>
145            </select>
146          </div>
147          <div className="flex items-center gap-1">
148            <p>Sort:</p>
149            <select
150              name="date"
151              id="date"
152              className="cursor-pointer"
153              value={sort}
154              onChange={(e) => {
155                setSort(e.target.value);
156              }}
157            >
158              <option value="oldest">Oldest</option>
159              <option value="newest">Newest</option>
160            </select>
161          </div>
162        </section>
163  
164        <div className="bg-primary-dark-green grid grid-cols-5 text-white text-sm p-4 rounded-t-xl mt-4 text-center">
165          <p>Order ID</p>
166          <p>Date</p>
167          <p>Time</p>
168          <p>Value</p>
169          <p>Status</p>
170        </div>
171        {renderTransactions()}
172      </section>
173    );
174  }