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: 2 additions & 1 deletion src/app/modules/modules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "@/auth";
import "@/dashboard";
import "@/deposits";
import "@/orders";

import "@/shared";
import "@/i18n";
import "@/shared";
4 changes: 2 additions & 2 deletions src/dashboard/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";

import { Deposit } from "@/deposits/components/Deposit";
import { Orders } from "@/orders/components/Orders";

import { Chart } from "../components/Chart";
import { Deposits } from "../components/Deposits";

export function DashboardPage() {
return (
Expand Down Expand Up @@ -32,7 +32,7 @@ export function DashboardPage() {
height: 240,
}}
>
<Deposits />
<Deposit />
</Paper>
</Grid>
{/* Recent Orders */}
Expand Down
19 changes: 19 additions & 0 deletions src/deposits/__mocks__/DepositsMother.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { faker } from "@faker-js/faker";

import { Deposit } from "../deposits.types";

export class DepositsMother {
static getDeposits(deposit?: Partial<Deposit>) {
return {
id: faker.string.uuid(),
amount: faker.finance.amount(),
name: faker.person.fullName(),
date: faker.date.past().toISOString(),
...deposit,
} as Deposit;
}

static getRandomList(length = 10, deposit?: Partial<Deposit>) {
return Array.from({ length }, () => this.getDeposits(deposit));
}
}
15 changes: 15 additions & 0 deletions src/deposits/assets/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"translation": {
"deposit.title": "Deposits",
"deposit.total": "Total deposits",
"deposit.balance": "View balance",
"deposit.recent": "Recent deposits",
"deposit.fetch.error": "Error fetching deposits",
"deposit.delete.error": "Error deleting deposit",
"deposit.delete.success": "Deposit successfully deleted",
"deposit.table.date": "DATE",
"deposit.table.id": "ID",
"deposit.table.name": "NAME",
"deposit.table.amount": "AMOUNT"
}
}
15 changes: 15 additions & 0 deletions src/deposits/assets/locales/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"translation": {
"deposit.title": "Depósitos",
"deposit.total": "Depósitos totales",
"deposit.balance": "Ver balance",
"deposit.recent": "Depósitos recientes",
"deposit.fetch.error": "Error al obtener los depósitos",
"deposit.delete.error": "Error al eliminar el depósito",
"deposit.delete.success": "Depósito eliminado con éxito",
"deposit.table.date": "FECHA",
"deposit.table.id": "ID",
"deposit.table.name": "NOMBRE",
"deposit.table.amount": "TOTAL"
}
}
9 changes: 9 additions & 0 deletions src/deposits/assets/locales/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LocaleResources } from "@/i18n/i18n.types";

import en from "./en.json";
import es from "./es.json";

export default {
en,
es,
} as LocaleResources;
55 changes: 55 additions & 0 deletions src/deposits/components/Deposit.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { I18nextProvider, initReactI18next } from "react-i18next";

import { waitFor } from "@testing-library/react";
import i18next from "i18next";
import { http, HttpResponse } from "msw";

import { server } from "@/mock-server/node";

import { Deposit } from "./Deposit";
import { DepositsMother } from "../__mocks__/DepositsMother";
import resources from "../assets/locales";

import { renderWithTestProviders } from "#/tests.helpers";

i18next.use(initReactI18next).init({
lng: "en",
fallbackLng: "en",
resources,
});

describe("Deposit", () => {
const deposits = DepositsMother.getRandomList();
const mockTotalAmount = deposits.reduce((total, deposit) => total + parseFloat(deposit.amount), 0);

beforeEach(() => {
//server mock return data
server.use(
http.get("/api/deposits", () =>
HttpResponse.json({
data: deposits,
}),
),
);
});

it("renders total amount correctly", async () => {
const { getByText } = renderWithTestProviders(
<I18nextProvider i18n={i18next}>
<Deposit />
</I18nextProvider>,
);

await waitFor(() => expect(getByText(`$${mockTotalAmount.toFixed(2)}`)).toBeInTheDocument());
});

it("renders title and balance link", () => {
const { getByText } = renderWithTestProviders(
<I18nextProvider i18n={i18next}>
<Deposit />
</I18nextProvider>,
);
expect(getByText("Total deposits")).toBeInTheDocument();
expect(getByText("View balance")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import * as React from "react";
import { useTranslation } from "react-i18next";

import Link from "@mui/material/Link";
import Typography from "@mui/material/Typography";

import { Title } from "@/shared/components/Title";

import { useDepositsControllers } from "../hooks/useDepositsControllers";

function preventDefault(event: React.MouseEvent) {
event.preventDefault();
}

export function Deposits() {
export function Deposit() {
const { t } = useTranslation();

const { totalAmount } = useDepositsControllers();

return (
<React.Fragment>
<Title>Recent Deposits</Title>
<Title>{t("deposit.total")}</Title>
<Typography component="p" variant="h4">
$3,024.00
${totalAmount.toFixed(2)}
</Typography>
<Typography color="text.secondary" sx={{ flex: 1 }}>
on 15 March, 2019
on {new Date().toLocaleDateString()}
</Typography>
<div>
<Link color="primary" href="#" onClick={preventDefault}>
View balance
{t("deposit.balance")}
</Link>
</div>
</React.Fragment>
Expand Down
42 changes: 42 additions & 0 deletions src/deposits/components/DepositsList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { screen, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";

import { server } from "@/mock-server/node";

import { DepositsList } from "./DepositsList";
import { DepositsMother } from "../__mocks__/DepositsMother";

import { renderWithTestProviders } from "#/tests.helpers";

vi.mock("react-i18next", () => ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useTranslation: () => ({ t: (key: any) => key }),
}));

describe("DepositList", () => {
const deposits = DepositsMother.getRandomList();

beforeEach(() => {
//server mock return data
server.use(
http.get("/api/deposits", () =>
HttpResponse.json({
data: deposits,
}),
),
);
});

it("renders table correctly with deposits", async () => {
const { container } = renderWithTestProviders(<DepositsList />);

// Verify colums title
expect(screen.getByText("Date")).toBeInTheDocument();
expect(screen.getByText("ID")).toBeInTheDocument();
expect(screen.getByText("Name")).toBeInTheDocument();
expect(screen.getByText("Amount")).toBeInTheDocument();

await waitFor(() => expect(screen.getByText(deposits[0].name)).toBeInTheDocument());
expect(container.querySelectorAll('[aria-label="delete"]')).toHaveLength(10);
});
});
53 changes: 53 additions & 0 deletions src/deposits/components/DepositsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useTranslation } from "react-i18next";

import DeleteIcon from "@mui/icons-material/Delete";
import { IconButton } from "@mui/material";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";

import { AllowedAuth } from "@/auth/components/AllowedAuth";
import { Title } from "@/shared/components/Title";

import { useDepositsControllers } from "../hooks/useDepositsControllers";

export function DepositsList() {
const { t } = useTranslation();
const { deposits, canDelete, handleDepositDelete } = useDepositsControllers();

return (
<>
<Title>{t("deposit.recent")}</Title>
<Table size="small" data-testid="deposits-table">
<TableHead>
<TableRow>
<TableCell>{t("deposit.table.date")}</TableCell>
<TableCell>{t("deposit.table.id")}</TableCell>
<TableCell>{t("deposit.table.name")}</TableCell>
<TableCell>{t("deposit.table.amount")}</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{deposits?.data.map((deposit) => (
<TableRow key={deposit.id}>
<TableCell>{new Date(deposit.date).toLocaleDateString()}</TableCell>
<TableCell>...{deposit.id.split("-").at(-1)}</TableCell>
<TableCell>{deposit.name}</TableCell>
<TableCell>${deposit.amount}</TableCell>
<TableCell align="right">
<AllowedAuth permissions={canDelete}>
<IconButton aria-label="delete" onClick={handleDepositDelete(deposit.id)}>
<DeleteIcon />
</IconButton>
</AllowedAuth>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
}
9 changes: 9 additions & 0 deletions src/deposits/deposits.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const MODULE_DEPOSITS = "deposits";

export const QUERY_KEY_DEPOSITS = "deposits";

export const ERROR_DEPOSITID_REQUIRED = "error:required:depositId";

export const PERMISSION_DEPOSITS_LIST = "deposits:list";
export const PERMISSION_DEPOSITS_VIEW = "deposits:view";
export const PERMISSION_DEPOSITS_DELETE = "deposits:delete";
40 changes: 40 additions & 0 deletions src/deposits/deposits.mock.handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { delay, http, HttpResponse } from "msw";

import { DEFAULT_DELAY } from "@/mock-server/constants";

import { DepositsMother } from "./__mocks__/DepositsMother";
import { ERROR_DEPOSITID_REQUIRED } from "./deposits.constants";

const deposits = DepositsMother.getRandomList();

export const handlers = [
http.get("/api/deposits", async () => {
await delay(DEFAULT_DELAY);

return HttpResponse.json({
data: deposits,
});
}),

http.delete("/api/deposits/:depositId", async ({ params }) => {
const depositId = params.depositId;

if (!depositId) {
return HttpResponse.json(
{
code: ERROR_DEPOSITID_REQUIRED,
message: "depositId is required",
},
{ status: 400 },
);
}

const index = deposits.findIndex((deposit) => deposit.id === depositId);
if (index > -1) {
deposits.splice(index, 1);
}

await delay(DEFAULT_DELAY);
return new HttpResponse(null, { status: 204 });
}),
];
15 changes: 15 additions & 0 deletions src/deposits/deposits.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import axios from "axios";

import { ApiListResponse, getEndpoint } from "@/app/api";

import { Deposit } from "./deposits.types";

export function fetchDeposits() {
return axios.get<ApiListResponse<Deposit[]>>(getEndpoint() + "deposits").then((res) => res.data);
}

export function deleteDeposit(depositId: string) {
return axios.delete(getEndpoint() + `deposits/${depositId}`, {
method: "DELETE",
});
}
6 changes: 6 additions & 0 deletions src/deposits/deposits.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Deposit {
id: string;
amount: string;
name: string;
date: string;
}
Loading