Магазин на JavaScript, часть 16 из 19. Личный кабинет, список заказов и отдельный заказ

30.01.2022

Теги: BackendExpress.jsFrontendJavaScriptNode.jsORMReact.jsБазаДанныхИнтернетМагазинКаталогТоваровКорзинаФреймворк

Теперь займемся личным кабинетом. Здесь много что можно сделать — история заказов в магазине, профили для удобного оформления, изменение пароля, избранные товары. Но мы ограничимся только историей заказов — а то конца и края не видно с этим магазином. Для начала добавим маршруты для просмотра списка заказов и отдельного заказа, потом создадим компоненты UserOrders.js и UserOrder.js.

Добавляем маршруты

Редактируем src/components/AppRoutes.js:

import { Routes, Route } from 'react-router-dom'
import Shop from '../pages/Shop.js'
import Login from '../pages/Login.js'
import Signup from '../pages/Signup.js'
import Basket from '../pages/Basket.js'
import Checkout from '../pages/Checkout.js'
import Product from '../pages/Product.js'
import Delivery from '../pages/Delivery.js'
import Contacts from '../pages/Contacts.js'
import NotFound from '../pages/NotFound.js'
import User from '../pages/User.js'
import UserOrders from '../pages/UserOrders.js'
import UserOrder from '../pages/UserOrder.js'
import Admin from '../pages/Admin.js'
import { AppContext } from './AppContext.js'
import { useContext } from 'react'
import { observer } from 'mobx-react-lite'

const publicRoutes = [
    {path: '/', Component: Shop},
    {path: '/login', Component: Login},
    {path: '/signup', Component: Signup},
    {path: '/product/:id', Component: Product},
    {path: '/basket', Component: Basket},
    {path: '/checkout', Component: Checkout},
    {path: '/delivery', Component: Delivery},
    {path: '/contacts', Component: Contacts},
    {path: '*', Component: NotFound},
]

const authRoutes = [
    {path: '/user', Component: User},
    {path: '/user/orders', Component: UserOrders},
    {path: '/user/order/:id', Component: UserOrder},
]

const adminRoutes = [
    {path: '/admin', Component: Admin},
]

const AppRouter = observer(() => {
    const { user } = useContext(AppContext)
    return (
        <Routes>
            {publicRoutes.map(({path, Component}) =>
                <Route key={path} path={path} element={<Component />} />
            )}
            {user.isAuth && authRoutes.map(({path, Component}) =>
                <Route key={path} path={path} element={<Component />} />
            )}
            {user.isAdmin && adminRoutes.map(({path, Component}) =>
                <Route key={path} path={path} element={<Component />} />
            )}
        </Routes>
    )
})

export default AppRouter

Создаем компоненты

Компонент src/pages/UserOrders.js

import { useState, useEffect } from 'react'
import { userGetAll as getAllOrders } from '../http/orderAPI.js'
import { Container, Spinner } from 'react-bootstrap'
import Orders from '../components/Orders.js'

const UserOrders = () => {
    const [orders, setOrders] = useState(null)
    const [fetching, setFetching] = useState(true)

    useEffect(() => {
        getAllOrders()
            .then(
                data => setOrders(data)
            )
            .finally(
                () => setFetching(false)
            )
    }, [])

    if (fetching) {
        return <Spinner animation="border" />
    }

    return (
        <Container>
            <h1>Ваши заказы</h1>
            <Orders items={orders} admin={false} />
        </Container>
    )
}

export default UserOrders

Компонент src/pages/UserOrder.js

import { useState, useEffect } from 'react'
import { userGetOne as getOneOrder } from '../http/orderAPI.js'
import { Container, Spinner } from 'react-bootstrap'
import Order from '../components/Order.js'
import { useParams } from 'react-router-dom'

const UserOrder = () => {
    const { id } = useParams()
    const [order, setOrder] = useState(null)
    const [fetching, setFetching] = useState(true)
    const [error, setError] = useState(null)

    useEffect(() => {
        getOneOrder(id)
            .then(
                data => setOrder(data)
            )
            .catch(
                error => setError(error.response.data.message)
            )
            .finally(
                () => setFetching(false)
            )
    }, [id])

    if (fetching) {
        return <Spinner animation="border" />
    }

    if (error) {
        return <p>{error}</p>
    }

    return (
        <Container>
            <h1>Заказ № {order.id}</h1>
            <Order data={order} admin={false} />
        </Container>
    )
}

export default UserOrder

Компонент src/components/Orders.js

import { Table } from 'react-bootstrap'
import { Link } from 'react-router-dom'

const Orders = (props) => {

    if (props.items.length === 0) {
        return <p>Список заказов пустой</p>
    }

    return (
        <Table bordered hover size="sm" className="mt-3">
            <thead>
                <tr>
                    <th></th>
                    <th>Дата</th>
                    <th>Покупатель</th>
                    <th>Адрес почты</th>
                    <th>Телефон</th>
                    <th>Статус</th>
                    <th>Сумма</th>
                    <th>Подробнее</th>
                </tr>
            </thead>
            <tbody>
                {props.items.map(item => 
                    <tr key={item.id}>
                        <td>{item.id}</td>
                        <td>{item.prettyCreatedAt}</td>
                        <td>{item.name}</td>
                        <td>{item.email}</td>
                        <td>{item.phone}</td>
                        <td>{item.status}</td>
                        <td>{item.amount}</td>
                        <td>
                            {props.admin ? (
                                <Link to={`/admin/order/${item.id}`}>Подробнее</Link>
                            ) : (
                                <Link to={`/user/order/${item.id}`}>Подробнее</Link>
                            )}
                            
                        </td>
                    </tr>
                )}
            </tbody>
        </Table>
    )
}

export default Orders

Компонент src/components/Order.js

import { Table } from 'react-bootstrap'

const Order = (props) => {
    return (
        <>
            <ul>
                <li>Дата заказа: {props.data.prettyCreatedAt}</li>
                <li>
                    Статус заказа:
                    {props.data.status === 0 && <span>Новый</span>}
                    {props.data.status === 1 && <span>В работе</span>}
                    {props.data.status === 2 && <span>Завершен</span>}
                </li>
            </ul>
            <ul>
                <li>Имя, фамилия: {props.data.name}</li>
                <li>Адрес почты: {props.data.email}</li>
                <li>Номер телефона: {props.data.phone}</li>
                <li>Адрес доставки: {props.data.address}</li>
                <li>Комментарий: {props.data.comment}</li>
            </ul>
            <Table bordered hover size="sm" className="mt-3">
                <thead>
                    <tr>
                        <th>Название</th>
                        <th>Цена</th>
                        <th>Кол-во</th>
                        <th>Сумма</th>
                    </tr>
                </thead>
                <tbody>
                    {props.data.items.map(item => 
                        <tr key={item.id}>
                            <td>{item.name}</td>
                            <td>{item.price}</td>
                            <td>{item.quantity}</td>
                            <td>{item.price * item.quantity}</td>
                        </tr>
                    )}
                    <tr>
                        <td colSpan={3}>Итого</td>
                        <td>{props.data.amount}</td>
                    </tr>
                </tbody>
            </Table>
        </>
    )
}

export default Order

Последние два компонента можно было и не создавать, а показать список заказов и отдельный заказ прямо в компонентах UserOrders и UserOrder. Но они нам потребуются при разработке панели управления для администратора магазина, потому что там тоже нужно показывать список заказов и отдельный заказ.

Формат даты заказа

Первый способ

Все бы ничего, но только дата заказа показывается как 2022-01-29T08:20:10.301Z, по стандарту ISO-8601 — что для обычного покупателя неудобно. Давайте изменим запрос к базе данных и будем форматоровать дату, чтобы получить что-то более привычное. Редактируем models/Order.js на сервере:

import sequelize from '../sequelize.js'
import { Order as OrderMapping } from './mapping.js'
import { OrderItem as OrderItemMapping } from './mapping.js'
import AppError from '../errors/AppError.js'

class Order {
    async getAll(userId = null) {
        const options = {
            attributes: {
                include: [
                    [
                        sequelize.fn(
                            'to_char',
                            sequelize.col('created_at'),
                            'DD.MM.YYYY HH24:MI'
                        ),
                        'prettyCreated',
                    ],
                ],
            },
        }
        if (userId) {
            options.where = {userId}
        }
        const orders = await OrderMapping.findAll(options)
        return orders
    }
    /* ..... */
}

export default new Order()

При работе с MySQL вместо функции to_char() используем функцию DATE_FORMAT():

class Order {
    async getAll(userId = null) {
        const options = {
            attributes: {
                include: [
                    sequelize.fn(
                        'DATE_FORMAT',
                        sequelize.col('created_at'),
                        '%d-%m-%Y %H:%i'
                    ),
                    'prettyCreated',
                ],
            },
        }
        if (userId) {
            options.where = {userId}
        }
        const orders = await OrderMapping.findAll(options)
        return orders
    }
    /* ..... */
}
Подробнее см. здесь (PostgreSQL), здесь (MySQL) и здесь (Sequelize).

Теперь на клиенте в компоненте UserOrders.js будем выводить не поле createdAt, а отформатированное поле prettyCreated, которое будет иметь вид 29.01.2022 08:20. Стало намного лучше, только время показывается по Гринвичу, потому как так мы храним в базе данных. Что в общем-то хорошо и правильно, но пользователей будет сбивать с толку.

Можно задать getter-ы для полей createdAt и updatedAt при определении модели «Заказ»:

// модель «Заказ», таблица БД «orders»
const Order = sequelize.define('order', {
    id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true},
    name: {type: DataTypes.STRING, allowNull: false},
    email: {type: DataTypes.STRING, allowNull: false},
    phone: {type: DataTypes.STRING, allowNull: false},
    address: {type: DataTypes.STRING, allowNull: false},
    amount: {type: DataTypes.INTEGER, allowNull: false},
    status: {type: DataTypes.INTEGER, allowNull: false, defaultValue: 0},
    comment: {type: DataTypes.STRING},
    createdAt: {
        type: DataTypes.DATE,
        get() {
            return this.getDataValue('createdAt').toLocaleString('ru-RU', {timeZone: 'Europe/Moscow'})
        }
    },
    updatedAt: {
        type: DataTypes.DATE,
        get() {
            return this.getDataValue('updatedAt').toLocaleString('ru-RU', {timeZone: 'Europe/Moscow'})
        }
    },
})

Но лучше указать временную зону в настройках server/sequelize.js, чтобы это работало для всех моделей:

import { Sequelize } from 'sequelize'

export default new Sequelize(
    process.env.DB_NAME, // база данных
    process.env.DB_USER, // пользователь
    process.env.DB_PASS, // пароль
    {
        dialect: 'postgres',
        host: process.env.DB_HOST,
        port: process.env.DB_PORT,
        define: {
            // в базе данных поля будут created_at и updated_at
            underscored: true
        },
        logging: false,
        timezone: 'Europe/Moscow',
    }
)

Второй способ

Неудобно городить такой огород с форматированием даты-времени для каждого запроса, если нужно показать их пользователю. Лучше при определении модели создадим два виртуальных поля prettyCreatedAt и prettyUpdatedAt — которые будут брать значение createdAt и updatedAt и форматировать под наши потребности.

// модель «Заказ», таблица БД «orders»
const Order = sequelize.define('order', {
    id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true},
    name: {type: DataTypes.STRING, allowNull: false},
    email: {type: DataTypes.STRING, allowNull: false},
    phone: {type: DataTypes.STRING, allowNull: false},
    address: {type: DataTypes.STRING, allowNull: false},
    amount: {type: DataTypes.INTEGER, allowNull: false},
    status: {type: DataTypes.INTEGER, allowNull: false, defaultValue: 0},
    comment: {type: DataTypes.STRING},
    prettyCreatedAt: {
        type: DataTypes.VIRTUAL,
        get() {
            return this.getDataValue('createdAt').toLocaleString('ru-RU')
        }
    },
    prettyUpdatedAt: {
        type: DataTypes.VIRTUAL,
        get() {
            return this.getDataValue('updatedAt').toLocaleString('ru-RU')
        }
    },
})

Если теперь вывести значение этого поля в компоненте UserOrders.js, то оно будет иметь вид 29.01.2022, 14:18:13. Но мы можем сделать любой формат:

// модель «Заказ», таблица БД «orders»
const Order = sequelize.define('order', {
    id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true},
    name: {type: DataTypes.STRING, allowNull: false},
    email: {type: DataTypes.STRING, allowNull: false},
    phone: {type: DataTypes.STRING, allowNull: false},
    address: {type: DataTypes.STRING, allowNull: false},
    amount: {type: DataTypes.INTEGER, allowNull: false},
    status: {type: DataTypes.INTEGER, allowNull: false, defaultValue: 0},
    comment: {type: DataTypes.STRING},
    prettyCreatedAt: {
        type: DataTypes.VIRTUAL,
        get() {
            const value = this.getDataValue('createdAt')
            const day = value.getDate()
            const month = value.getMonth() + 1
            const year = value.getFullYear()
            const hours = value.getHours()
            const minutes = value.getMinutes()
            return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes
        }
    },
    prettyUpdatedAt: {
        type: DataTypes.VIRTUAL,
        get() {
            const value = this.getDataValue('updatedAt')
            const day = value.getDate()
            const month = value.getMonth() + 1
            const year = value.getFullYear()
            const hours = value.getHours()
            const minutes = value.getMinutes()
            return day + '.' + month + '.' + year + ' ' + hours + ':' + minutes
        }
    },
})

Теперь нам не нужно в запросах думать о форматировании даты-времени, поэтому на сервере в модели models/Order.js вернем все, как было изначально.

import { Order as OrderMapping } from './mapping.js'
import { OrderItem as OrderItemMapping } from './mapping.js'
import AppError from '../errors/AppError.js'

class Order {
    async getAll(userId = null) {
        const options = {}
        if (userId) options.where = {userId}
        const orders = await OrderMapping.findAll(options)
        return orders
    }
    /* ..... */
}

export default new Order()

Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.