Разработка собственного настраиваемого генератора счетов-фактур с помощью Refine и Strapi | Часть I

Введение

Мы собираемся разработать приложение генератора счетов для нашего бизнеса с помощью Refine и Strapi. Давайте вместе посмотрим, насколько простым, но функциональным оно может быть!

Эта статья будет состоять из двух частей, и мы постараемся подробно объяснить каждый шаг. В этой части мы создадим основные части нашего приложения.

В этой части мы создадим панель, где будет размещена информация о нашей собственной компании, где мы сможем создавать клиентов и создавать контакты с компаниями-клиентами.

Создание проекта Refine

Давайте начнем с создания нашего проекта Refine. Вы можете использовать супершаблон для создания проекта refine.

npx superplate-cli -p refine-react refine-invoice-genarator
Вход в полноэкранный режим Выход из полноэкранного режима
✔ What will be the name of your app ·refine-invoice-genarator
✔ Package manager: · npm
✔ Do you want to using UI Framework? > Yes, I want Ant Design
✔ Do you want to customize theme?: … no
✔ Data Provider: Strapi
✔ Do you want to customize layout? … no
✔ i18n - Internationalization: · no
Войдите в полноэкранный режим Выйти из полноэкранного режима

superplate быстро создаст наш проект refine в соответствии с выбранными нами функциями. Давайте продолжим, установив refine Strapi-v4 Data Provider, который мы будем использовать позже.

npm i @pankod/refine-strapi-v4
Войти в полноэкранный режим Выход из полноэкранного режима

Наш проект refine и установки теперь готовы! Давайте начнем его использовать.

Использование

src/authProvider.ts:

import { AuthProvider } from "@pankod/refine-core";
import { AuthHelper } from "@pankod/refine-strapi-v4";

import { TOKEN_KEY, API_URL } from "./constants";

import axios from "axios";

export const axiosInstance = axios.create();
const strapiAuthHelper = AuthHelper(API_URL + "/api");

export const authProvider: AuthProvider = {
    login: async ({ username, password }) => {
        const { data, status } = await strapiAuthHelper.login(
            username,
            password,
        );
        if (status === 200) {
            localStorage.setItem(TOKEN_KEY, data.jwt);

            // set header axios instance
            axiosInstance.defaults.headers = {
                Authorization: `Bearer ${data.jwt}`,
            };

            return Promise.resolve();
        }
        return Promise.reject();
    },
    logout: () => {
        localStorage.removeItem(TOKEN_KEY);
        return Promise.resolve();
    },
    checkError: () => Promise.resolve(),
    checkAuth: () => {
        const token = localStorage.getItem(TOKEN_KEY);
        if (token) {
            axiosInstance.defaults.headers = {
                Authorization: `Bearer ${token}`,
            };
            return Promise.resolve();
        }

        return Promise.reject();
    },
    getPermissions: () => Promise.resolve(),
    getUserIdentity: async () => {
        const token = localStorage.getItem(TOKEN_KEY);
        if (!token) {
            return Promise.reject();
        }

        const { data, status } = await strapiAuthHelper.me(token);
        if (status === 200) {
            const { id, username, email } = data;
            return Promise.resolve({
                id,
                username,
                email,
            });
        }

        return Promise.reject();
    },
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Настройте Refine для Strapi-v4

src/App.tsx:

import { Refine } from "@pankod/refine-core";
import { notificationProvider, Layout, LoginPage } from "@pankod/refine-antd";
import routerProvider from "@pankod/refine-react-router";

import { DataProvider } from "@pankod/refine-strapi-v4";
import { authProvider, axiosInstance } from "./authProvider";

import "@pankod/refine-antd/dist/styles.min.css";

function App() {
    const API_URL = "Your_Strapi_Url";
    const dataProvider = DataProvider(API_URL + "/api", axiosInstance);

    return (
        <Refine
            routerProvider={routerProvider}
            notificationProvider={notificationProvider}
            Layout={Layout}
            dataProvider={dataProvider}
            authProvider={authProvider}
            LoginPage={LoginPage}
        />
    );
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Создание коллекций на Strapi

Мы создали три коллекции на Strapi: company, client и contact и добавили связь между ними. Подробную информацию о том, как создать коллекцию, вы можете найти здесь.

Company:

  • Логотип: Media
  • Имя: Текст
  • Адрес: Текст
  • Страна: Текст
  • Город: Текст
  • электронная почта: Email
  • Веб-сайт: Текст

Client:

  • Имя: Текст
  • Контакты: Отношения с контактом

Contact:

  • Имя_имя: Текст
  • Фамилия: Текст
  • Номер_телефона: Текст
  • Электронная почта: электронная почта
  • Работа: Текст
  • Клиент: Отношения с клиентом

Мы создали наши коллекции с помощью Strapi, теперь мы можем создать клиентов и их контакты с помощью refine.

Страница подробной информации о вашей компании

В качестве первого шага, давайте начнем создавать часть, где будет располагаться наша собственная Компания. Если есть другие компании, которыми вам нужно управлять, вы можете создать их на странице «Ваша компания» и просмотреть их здесь.

Компонент карточки компании

Давайте разработаем компонент, который будет содержать подробную информацию о нашей компании. Затем отобразим его с помощью refine-antd List. Мы поместим в компонент Card такую информацию, как название, логотип и адрес из коллекции Company, которую мы создали на Strapi.

src/components/company/CompanyItem.tsx:

import {
    Card,
    DeleteButton,
    UrlField,
    EmailField,
    EditButton,
    Typography,
} from "@pankod/refine-antd";

import { ICompany } from "interfaces";
import { API_URL } from "../../constants";

const { Title, Text } = Typography;

type CompanyItemProps = {
    item: ICompany;
};

export const CompanyItem: React.FC<CompanyItemProps> = ({ item }) => {
    const image = item.logo ? API_URL + item.logo.url : "./error.png";

    return (
        <Card
            style={{ width: "300px" }}
            cover={
                <div style={{ display: "flex", justifyContent: "center" }}>
                    <img
                        style={{
                            width: 220,
                            height: 100,
                            padding: 24,
                        }}
                        src={image}
                        alt="logo"
                    />
                </div>
            }
            actions={[
                <EditButton key="edit" size="small" hideText />,
                <DeleteButton
                    key="delete"
                    size="small"
                    hideText
                    recordItemId={item.id}
                />,
            ]}
        >
            <Title level={5}>Company Name:</Title>
            <Text>{item.name}</Text>
            <Title level={5}>Company Address:</Title>
            <Text>{item.address}</Text>
            <Title level={5}>County:</Title>
            <Text>{item.country}</Text>
            <Title level={5}>City:</Title>
            <Text>{item.city}</Text>
            <Title level={5}>Email:</Title>
            <EmailField value={item.email} />
            <Title level={5}>Website:</Title>
            <UrlField value={item.website} />
        </Card>
    );
};
Вход в полноэкранный режим Выход из полноэкранного режима

Страница списка компаний

Поместим компонент CompanyItem, который мы создали выше, в уточненный список (refine-antd List) и отобразим информацию о компании.

src/pages/company/CompanyList.tsx:

import { IResourceComponentsProps } from "@pankod/refine-core";
import { useSimpleList, AntdList, List } from "@pankod/refine-antd";

import { CompanyItem } from "components/company";

export const CompanyList: React.FC<IResourceComponentsProps> = () => {
    const { listProps } = useSimpleList<ICompany>({
        metaData: { populate: ["logo"] },
    });

    return (
        <List title={"Your Companies"}>
            <AntdList
                grid={{ gutter: 16 }}
                {...listProps}
                renderItem={(item) => (
                    <AntdList.Item>
                        <CompanyItem item={item} />
                    </AntdList.Item>
                )}
            />
        </List>
    );
};
Вход в полноэкранный режим Выход из полноэкранного режима

src/App.tsx:

...

import { CompanyList } from "pages/company";

function App() {
    const API_URL = "Your_Strapi_Url";
    const dataProvider = DataProvider(API_URL + "/api", axiosInstance);

    return (
        <Refine
            routerProvider={routerProvider}
            notificationProvider={notificationProvider}
            Layout={Layout}
            dataProvider={dataProvider}
            authProvider={authProvider}
            LoginPage={LoginPage}
            resources={[
                {
                    name: "companies",
                    options: { label: "Your Company" },
                    list: CompanyList,
                },
            ]}
        />
    );
}
Войти в полноэкранный режим Выход из полноэкранного режима

Мы получаем данные из коллекции Company, которую мы создали в Strapi, благодаря уточнению dataProvider, и помещаем их в созданный нами компонент карточки.

Контактная страница

Наша Contact Page — это страница, связанная с Clients. Связь с компаниями-клиентами будет осуществляться через контакты, которые мы создадим здесь. Страница контактов будет содержать информацию о людях, с которыми мы будем связываться. Давайте создадим наш список с помощью хука refine useTable.

src/pages/contact/ContactList.tsx:

import {
    List,
    Table,
    TagField,
    useTable,
    Space,
    EditButton,
    DeleteButton,
    useModalForm,
} from "@pankod/refine-antd";

import { IContact } from "interfaces";
import { CreateContact } from "components/contacts";

export const ContactsList: React.FC = () => {
    const { tableProps } = useTable<IContact>({
        metaData: { populate: ["client"] },
    });

    const {
        formProps: createContactFormProps,
        modalProps,
        show,
    } = useModalForm({
        resource: "contacts",
        action: "create",
        redirect: false,
    });

    return (
        <>
            <List
                createButtonProps={{
                    onClick: () => {
                        show();
                    },
                }}
            >
                <Table {...tableProps} rowKey="id">
                    <Table.Column dataIndex="id" title="ID" />
                    <Table.Column dataIndex="first_name" title="First Name" />
                    <Table.Column dataIndex="last_name" title="Last Name" />
                    <Table.Column
                        dataIndex="phone_number"
                        title="Phone Number"
                    />
                    <Table.Column dataIndex="email" title="Email" />
                    <Table.Column
                        dataIndex="job"
                        title="Job"
                        render={(value: string) => (
                            <TagField color={"blue"} value={value} />
                        )}
                    />
                    <Table.Column<{ id: string }>
                        title="Actions"
                        dataIndex="actions"
                        render={(_, record) => (
                            <Space>
                                <EditButton
                                    hideText
                                    size="small"
                                    recordItemId={record.id}
                                />
                                <DeleteButton
                                    hideText
                                    size="small"
                                    recordItemId={record.id}
                                />
                            </Space>
                        )}
                    />
                </Table>
            </List>
            <CreateContact
                modalProps={modalProps}
                formProps={createContactFormProps}
            />
        </>
    );
};
Вход в полноэкранный режим Выход из полноэкранного режима

Страница списка клиентов

Выше мы создали пример компании и контактов. Теперь давайте создадим Client List, где мы сможем просматривать наших клиентов.

Компонент карточки клиента

Давайте разработаем карточки, которые будут отображаться в списке клиентов.

src/components/client/ClientItem.tsx:

import { useDelete } from "@pankod/refine-core";
import {
    Card,
    TagField,
    Typography,
    Dropdown,
    Menu,
    Icons,
} from "@pankod/refine-antd";

import { IClient } from "interfaces";

const { FormOutlined, DeleteOutlined } = Icons;
const { Title, Text } = Typography;

type ClientItemProps = {
    item: IClient;
    editShow: (id?: string | undefined) => void;
};

export const ClientItem: React.FC<ClientItemProps> = ({ item, editShow }) => {
    const { mutate } = useDelete();

    return (
        <Card style={{ width: 300, height: 300, borderColor: "black" }}>
            <div style={{ position: "absolute", top: "10px", right: "5px" }}>
                <Dropdown
                    overlay={
                        <Menu mode="vertical">
                            <Menu.Item
                                key="1"
                                style={{
                                    fontWeight: 500,
                                }}
                                icon={
                                    <FormOutlined
                                        style={{
                                            color: "green",
                                        }}
                                    />
                                }
                                onClick={() => editShow(item.id)}
                            >
                                Edit Client
                            </Menu.Item>
                            <Menu.Item
                                key="2"
                                style={{
                                    fontWeight: 500,
                                }}
                                icon={
                                    <DeleteOutlined
                                        style={{
                                            color: "red",
                                        }}
                                    />
                                }
                                onClick={() =>
                                    mutate({
                                        resource: "clients",
                                        id: item.id,
                                        mutationMode: "undoable",
                                        undoableTimeout: 5000,
                                    })
                                }
                            >
                                Delete Client
                            </Menu.Item>
                        </Menu>
                    }
                    trigger={["click"]}
                >
                    <Icons.MoreOutlined
                        style={{
                            fontSize: 24,
                        }}
                    />
                </Dropdown>
            </div>

            <Title level={4}>{item.name}</Title>
            <Title level={5}>Client Id:</Title>
            <Text>{item.id}</Text>
            <Title level={5}>Contacts:</Title>

            {item.contacts.map((item) => {
                return (
                    <TagField
                        color={"#d1c4e9"}
                        value={`${item.first_name} ${item.last_name}`}
                    />
                );
            })}
        </Card>
    );
};
Вход в полноэкранный режим Выход из полноэкранного режима

Страница создания и редактирования клиента

Страница клиента — это место, где вы можете обновлять информацию о своих клиентах и добавлять новых клиентов. Давайте создадим страницы «Создать» и «Редактировать» для создания новых и обновления существующих клиентов.

  • Создать клиента

src/components/client/CreateClient.tsx

import {
    Create,
    Drawer,
    DrawerProps,
    Form,
    FormProps,
    Input,
    ButtonProps,
    Grid,
    Select,
    useSelect,
    useModalForm,
    Button,
} from "@pankod/refine-antd";

import { IContact } from "interfaces";
import { CreateContact } from "components/contacts";

type CreateClientProps = {
    drawerProps: DrawerProps;
    formProps: FormProps;
    saveButtonProps: ButtonProps;
};

export const CreateClient: React.FC<CreateClientProps> = ({
    drawerProps,
    formProps,
    saveButtonProps,
}) => {
    const breakpoint = Grid.useBreakpoint();

    const { selectProps } = useSelect<IContact>({
        resource: "contacts",
        optionLabel: "first_name",
    });

    const {
        formProps: createContactFormProps,
        modalProps,
        show,
    } = useModalForm({
        resource: "contacts",
        action: "create",
        redirect: false,
    });

    return (
        <>
            <Drawer
                {...drawerProps}
                width={breakpoint.sm ? "500px" : "100%"}
                bodyStyle={{ padding: 0 }}
            >
                <Create saveButtonProps={saveButtonProps}>
                    <Form
                        {...formProps}
                        layout="vertical"
                        initialValues={{
                            isActive: true,
                        }}
                    >
                        <Form.Item
                            label="Client Company Name"
                            name="name"
                            rules={[
                                {
                                    required: true,
                                },
                            ]}
                        >
                            <Input />
                        </Form.Item>
                        <Form.Item label="Select Contact">
                            <div style={{ display: "flex" }}>
                                <Form.Item name={"contacts"} noStyle>
                                    <Select {...selectProps} mode="multiple" />
                                </Form.Item>
                                <Button type="link" onClick={() => show()}>
                                    Create Contact
                                </Button>
                            </div>
                        </Form.Item>
                    </Form>
                </Create>
            </Drawer>

            <CreateContact
                modalProps={modalProps}
                formProps={createContactFormProps}
            />
        </>
    );
};
Вход в полноэкранный режим Выход из полноэкранного режима
  • Редактировать клиента

src/components/client/EditClient.tsx:

import {
    Edit,
    Drawer,
    DrawerProps,
    Form,
    FormProps,
    Input,
    ButtonProps,
    Grid,
    Select,
    useSelect,
} from "@pankod/refine-antd";

type EditClientProps = {
    drawerProps: DrawerProps;
    formProps: FormProps;
    saveButtonProps: ButtonProps;
};

export const EditClient: React.FC<EditClientProps> = ({
    drawerProps,
    formProps,
    saveButtonProps,
}) => {
    const breakpoint = Grid.useBreakpoint();

    const { selectProps } = useSelect({
        resource: "contacts",
        optionLabel: "first_name",
    });

    return (
        <Drawer
            {...drawerProps}
            width={breakpoint.sm ? "500px" : "100%"}
            bodyStyle={{ padding: 0 }}
        >
            <Edit saveButtonProps={saveButtonProps}>
                <Form
                    {...formProps}
                    layout="vertical"
                    initialValues={{
                        isActive: true,
                    }}
                >
                    <Form.Item
                        label="Client Company Name"
                        name="name"
                        rules={[
                            {
                                required: true,
                            },
                        ]}
                    >
                        <Input />
                    </Form.Item>
                    <Form.Item label="Select Contact" name="contacts">
                        <Select {...selectProps} mode="multiple" />
                    </Form.Item>
                </Form>
            </Edit>
        </Drawer>
    );
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Страница списка клиентов

Выше мы создали компоненты Card, Create и Edit. Давайте определим и используем эти компоненты, созданные нами, в нашем ClientList.

src/pages/client/ClientList.tsx:

import { IResourceComponentsProps, HttpError } from "@pankod/refine-core";

import {
    useSimpleList,
    AntdList,
    List,
    useDrawerForm,
    CreateButton,
} from "@pankod/refine-antd";

import { IClient } from "interfaces";
import { ClientItem, CreateClient, EditClient } from "components/client";

export const ClientList: React.FC<IResourceComponentsProps> = () => {
    const { listProps } = useSimpleList<IClient>({
        metaData: { populate: ["contacts"] },
    });

    const {
        drawerProps: createDrawerProps,
        formProps: createFormProps,
        saveButtonProps: createSaveButtonProps,
        show: createShow,
    } = useDrawerForm<IClient, HttpError, IClient>({
        action: "create",
        resource: "clients",
        redirect: false,
    });

    const {
        drawerProps: editDrawerProps,
        formProps: editFormProps,
        saveButtonProps: editSaveButtonProps,
        show: editShow,
    } = useDrawerForm<IClient, HttpError, IClient>({
        action: "edit",
        resource: "clients",
        redirect: false,
    });

    return (
        <>
            <List
                pageHeaderProps={{
                    extra: <CreateButton onClick={() => createShow()} />,
                }}
            >
                <AntdList
                    grid={{ gutter: 24, xs: 1 }}
                    {...listProps}
                    renderItem={(item) => (
                        <AntdList.Item>
                            <ClientItem item={item} editShow={editShow} />
                        </AntdList.Item>
                    )}
                />
            </List>
            <CreateClient
                drawerProps={createDrawerProps}
                formProps={createFormProps}
                saveButtonProps={createSaveButtonProps}
            />
            <EditClient
                drawerProps={editDrawerProps}
                formProps={editFormProps}
                saveButtonProps={editSaveButtonProps}
            />
        </>
    );
};
Вход в полноэкранный режим Выход из полноэкранного режима

Мы создали наши страницы Client и Contact. Теперь давайте создадим клиента с уточнением и определим контакты для наших клиентов.

Заключение

Мы завершили первый этап нашего проекта, создав базовую платформу для пользователей для создания своей компании и клиентов. В следующем разделе мы добавим больше функциональности в эту программу, позволив пользователям генерировать счета и отслеживать платежи. Оставайтесь с нами, поскольку мы продолжаем работу над Refine Invoice Generator!

Для получения дополнительной информации о программе refine ->

Живой пример CodeSandbox

Вы можете найти статью Refine Invoice Generator Part II здесь →

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *