From b938f296c113697bc1eee4ff6fa365adbff8331b Mon Sep 17 00:00:00 2001 From: Sam Liu Date: Wed, 3 Jul 2024 08:57:50 +0000 Subject: [PATCH] [FWA-5] Home Create and List --- ...bca538155d_create_house_and_area_table.py} | 18 ++--- .../4b90a6ac504b_create_users_table.py | 4 +- backend/app.py | 6 ++ backend/core/dependencies/dependencies.py | 2 +- backend/core/message_code.py | 3 + backend/db/models/_model_base.py | 3 + backend/db/models/houses/houses.py | 18 ++--- backend/db/models/users/users.py | 6 +- backend/repos/repository_houses.py | 16 ++++- backend/repos/seeder/init_users.py | 3 - backend/routes/house/house.py | 18 ++++- backend/routes/user/user.py | 2 +- backend/schemas/house/house.py | 15 +++++ backend/schemas/user/user.py | 3 +- backend/services/house/house_service.py | 6 ++ frontend/src/api/house.js | 16 +++++ frontend/src/api/url.js | 3 + frontend/src/components/AreaAdd/AreaAdd.jsx | 6 +- frontend/src/components/AreaAdd/AreaItem.jsx | 4 +- frontend/src/components/Header.jsx | 3 +- frontend/src/components/Navbar/Navbar.jsx | 20 +++--- .../common/Pagination/Pagination.jsx | 64 ++++++++++++++++++ .../src/components/common/Pagination/index.js | 2 + frontend/src/hooks/usePagination.js | 60 +++++++++++++++++ frontend/src/lang/vi.json | 34 ++++++---- frontend/src/pages/Home.jsx | 8 ++- frontend/src/pages/House/House.jsx | 66 ++++++++++++++++++- .../src/pages/HouseCreate/HouseCreate.jsx | 47 +++++++++++-- frontend/src/pages/Layout.jsx | 2 +- frontend/src/pages/Profile/Profile.jsx | 13 ++-- frontend/src/routes/routes.js | 10 ++- frontend/src/utils/helper.js | 23 ++++--- frontend/tailwind.config.js | 3 + 33 files changed, 414 insertions(+), 93 deletions(-) rename alembic/versions/{7ef4aef5fcda_create_house_and_area_table.py => 0fbca538155d_create_house_and_area_table.py} (76%) create mode 100644 frontend/src/api/house.js create mode 100644 frontend/src/components/common/Pagination/Pagination.jsx create mode 100644 frontend/src/components/common/Pagination/index.js create mode 100644 frontend/src/hooks/usePagination.js diff --git a/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py b/alembic/versions/0fbca538155d_create_house_and_area_table.py similarity index 76% rename from alembic/versions/7ef4aef5fcda_create_house_and_area_table.py rename to alembic/versions/0fbca538155d_create_house_and_area_table.py index 6eae2f2..ccaeac4 100644 --- a/alembic/versions/7ef4aef5fcda_create_house_and_area_table.py +++ b/alembic/versions/0fbca538155d_create_house_and_area_table.py @@ -1,8 +1,8 @@ """create house and area table -Revision ID: 7ef4aef5fcda +Revision ID: 0fbca538155d Revises: 4b90a6ac504b -Create Date: 2024-06-25 09:20:59.915212 +Create Date: 2024-06-30 05:38:20.062935 """ from typing import Sequence, Union @@ -13,7 +13,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '7ef4aef5fcda' +revision: str = '0fbca538155d' down_revision: Union[str, None] = '4b90a6ac504b' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -28,11 +28,10 @@ def upgrade() -> None: sa.Column('address', sa.String(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id') ) - op.create_index(op.f('ix_houses_address'), 'houses', ['address'], unique=False) op.create_index(op.f('ix_houses_created_at'), 'houses', ['created_at'], unique=False) - op.create_index(op.f('ix_houses_icon'), 'houses', ['icon'], unique=False) op.create_index(op.f('ix_houses_id'), 'houses', ['id'], unique=False) op.create_index(op.f('ix_houses_name'), 'houses', ['name'], unique=False) op.create_table('areas', @@ -42,12 +41,11 @@ def upgrade() -> None: sa.Column('desc', sa.String(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['house_id'], ['houses.id'], ), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['house_id'], ['houses.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_areas_created_at'), 'areas', ['created_at'], unique=False) - op.create_index(op.f('ix_areas_desc'), 'areas', ['desc'], unique=False) - op.create_index(op.f('ix_areas_house_id'), 'areas', ['house_id'], unique=False) op.create_index(op.f('ix_areas_id'), 'areas', ['id'], unique=False) op.create_index(op.f('ix_areas_name'), 'areas', ['name'], unique=False) # ### end Alembic commands ### @@ -57,14 +55,10 @@ def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f('ix_areas_name'), table_name='areas') op.drop_index(op.f('ix_areas_id'), table_name='areas') - op.drop_index(op.f('ix_areas_house_id'), table_name='areas') - op.drop_index(op.f('ix_areas_desc'), table_name='areas') op.drop_index(op.f('ix_areas_created_at'), table_name='areas') op.drop_table('areas') op.drop_index(op.f('ix_houses_name'), table_name='houses') op.drop_index(op.f('ix_houses_id'), table_name='houses') - op.drop_index(op.f('ix_houses_icon'), table_name='houses') op.drop_index(op.f('ix_houses_created_at'), table_name='houses') - op.drop_index(op.f('ix_houses_address'), table_name='houses') op.drop_table('houses') # ### end Alembic commands ### diff --git a/alembic/versions/4b90a6ac504b_create_users_table.py b/alembic/versions/4b90a6ac504b_create_users_table.py index 9e5e5f1..2fe315f 100644 --- a/alembic/versions/4b90a6ac504b_create_users_table.py +++ b/alembic/versions/4b90a6ac504b_create_users_table.py @@ -1,7 +1,7 @@ """create users table Revision ID: 4b90a6ac504b -Revises: +Revises: Create Date: 2024-06-25 09:14:34.465698 """ @@ -27,7 +27,7 @@ def upgrade() -> None: sa.Column('password', sa.String(), nullable=False), sa.Column('name', sa.String(), nullable=True), sa.Column('is_admin', sa.Boolean(), nullable=True), - sa.Column('is_lock', sa.Boolean(), nullable=True), + sa.Column('is_lock', sa.DateTime(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id') diff --git a/backend/app.py b/backend/app.py index c491ab7..3ed125a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,6 +1,7 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from fastapi import FastAPI, Request, HTTPException +from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware @@ -70,6 +71,11 @@ async def unicorn_exception_handler(request: Request, exc: HTTPException): content={"status": exc.status_code, "data": exc.detail}, ) +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + print(exc) + return JSONResponse(status_code=422, content={"status": 422, "data": str(exc)}) + def api_routers(): app.include_router(router) diff --git a/backend/core/dependencies/dependencies.py b/backend/core/dependencies/dependencies.py index ab21bf5..2aa3cf6 100644 --- a/backend/core/dependencies/dependencies.py +++ b/backend/core/dependencies/dependencies.py @@ -30,7 +30,7 @@ async def is_logged_in(token: str = Depends(oauth2_scheme_soft_fail)) -> bool: user = user_service.get_by_id(user_id) if not user: raise credentials_exception - if user.is_lock is True: + if user.is_lock is not None: raise HTTPException(status_code=status.HTTP_423_LOCKED, detail=MessageCode.ACCOUNT_LOCK) except Exception: return credentials_exception diff --git a/backend/core/message_code.py b/backend/core/message_code.py index 11abb5a..1f0eb8c 100644 --- a/backend/core/message_code.py +++ b/backend/core/message_code.py @@ -1,6 +1,9 @@ class MessageCode(): + CREATE_USER_SUCCESS: str = 'CREATE_USER_SUCCESS' CREATED_USER: str = 'CREATED_USER' WRONG_INPUT: str = 'LOGIN_WRONG' ACCOUNT_LOCK: str = 'USER_LOCK' REFRESH_TOKEN_EXPIRED: str = 'REFRESH_TOKEN_EXPIRED' + CREATE_HOUSE_FAIL: str = 'CREATE_HOUSE_FAIL' + CREATE_HOUSE_SUCCESS: str = 'CREATE_HOUSE_SUCCESS' diff --git a/backend/db/models/_model_base.py b/backend/db/models/_model_base.py index d89b830..fca5bc7 100644 --- a/backend/db/models/_model_base.py +++ b/backend/db/models/_model_base.py @@ -18,3 +18,6 @@ class SqlAlchemyBase(Model): @classmethod def normalize(cls, val: str) -> str: return unidecode(val).lower().strip() + +class DeleteMixin: + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) diff --git a/backend/db/models/houses/houses.py b/backend/db/models/houses/houses.py index b668a04..4f3b099 100644 --- a/backend/db/models/houses/houses.py +++ b/backend/db/models/houses/houses.py @@ -4,30 +4,30 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from backend.db.models.guid import GUID -from .._model_base import SqlAlchemyBase +from .._model_base import SqlAlchemyBase, DeleteMixin -class Houses(SqlAlchemyBase): +class Houses(SqlAlchemyBase, DeleteMixin): __tablename__ = 'houses' id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate, index=True) - icon: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + icon: Mapped[str | None] = mapped_column(String, nullable=False) name: Mapped[str | None] = mapped_column(String, index=True, nullable=False) - address: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + address: Mapped[str | None] = mapped_column(String, nullable=False) - areas: Mapped[List["Areas"]] = relationship() + areas: Mapped[List["Areas"]] = relationship("Areas", back_populates="house", cascade="all, delete", passive_deletes=True) def __repr__(self): return f"{self.__class__.__name__}, name: {self.name}" -class Areas(SqlAlchemyBase): +class Areas(SqlAlchemyBase, DeleteMixin): __tablename__ = 'areas' id: Mapped[GUID] = mapped_column(GUID, primary_key=True, default=GUID.generate, index=True) - house_id: Mapped[GUID] = mapped_column(GUID, ForeignKey('houses.id'), index=True, nullable=False) + house_id: Mapped[GUID] = mapped_column(GUID, ForeignKey('houses.id', ondelete='CASCADE'), nullable=False) name: Mapped[str | None] = mapped_column(String, index=True, nullable=False) - desc: Mapped[str | None] = mapped_column(String, index=True, nullable=False) + desc: Mapped[str | None] = mapped_column(String, nullable=False) - user: Mapped['Houses'] = relationship(back_populates="areas") + house: Mapped['Houses'] = relationship(back_populates="areas") def __repr__(self): return f"{self.__class__.__name__}, name: {self.name}" diff --git a/backend/db/models/users/users.py b/backend/db/models/users/users.py index 28dde12..3862947 100644 --- a/backend/db/models/users/users.py +++ b/backend/db/models/users/users.py @@ -1,4 +1,6 @@ -from sqlalchemy import Boolean, String +from datetime import datetime + +from sqlalchemy import Boolean, String, DateTime from sqlalchemy.orm import Mapped, mapped_column from backend.db.models.guid import GUID @@ -13,7 +15,7 @@ class Users(SqlAlchemyBase): password: Mapped[str | None] = mapped_column(String, index=True, nullable=False) name: Mapped[str | None] = mapped_column(String, index=True, nullable=True) is_admin: Mapped[bool | None] = mapped_column(Boolean, default=False) - is_lock: Mapped[bool | None] = mapped_column(Boolean, default=False) + is_lock: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) def __repr__(self): return f"{self.__class__.__name__}, name: {self.name}, username: {self.username}" diff --git a/backend/repos/repository_houses.py b/backend/repos/repository_houses.py index 2008169..1381304 100644 --- a/backend/repos/repository_houses.py +++ b/backend/repos/repository_houses.py @@ -1,4 +1,4 @@ -from backend.db.models.houses import Houses +from backend.db.models.houses import Houses, Areas from sqlalchemy.orm import Session from backend.schemas.house import HouseCreate @@ -6,16 +6,26 @@ from backend.schemas.house import HouseCreate class RepositoryHouses: def __init__(self): self.houses = Houses() + self.areas = Areas() def get_all(self, skip: int = 0, limit: int = 100): - return self.houses.query.offset(skip).limit(limit).all() + return self.houses.query.filter_by(deleted_at=None).offset(skip).limit(limit).all() + + def get_all_areas(self, house_id: str, skip: int = 0, limit: int = 100): + return self.areas.query.filter_by(deleted_at=None, house_id=house_id).offset(skip).limit(limit).all() + + def get_count_all(self, skip: int = 0, limit: int = 100): + return self.houses.query.filter_by(deleted_at=None).offset(skip).limit(limit).count() def get_by_id(self, house_id: str): return self.houses.query.filter_by(id=house_id).one() def create(self, db: Session, house: HouseCreate): try: - db_house = Houses(**house.dict()) + areas = getattr(house, "areas") + db_house = Houses(**house.dict(exclude={"areas"})) + for area in areas: + db_house.areas.append(Areas(**area.dict())) db.add(db_house) db.commit() except Exception: diff --git a/backend/repos/seeder/init_users.py b/backend/repos/seeder/init_users.py index 46e7e80..b21f6c6 100644 --- a/backend/repos/seeder/init_users.py +++ b/backend/repos/seeder/init_users.py @@ -16,21 +16,18 @@ def dev_users() -> list[dict]: "password": "admin", "name": "Sam", "is_admin": True, - "is_lock": False, }, { "username": "duy", "password": "admin", "name": "Duy", "is_admin": True, - "is_lock": False, }, { "username": "sam1", "password": "admin", "name": "Sam1", "is_admin": False, - "is_lock": False, }, ] diff --git a/backend/routes/house/house.py b/backend/routes/house/house.py index dc050b1..102f844 100644 --- a/backend/routes/house/house.py +++ b/backend/routes/house/house.py @@ -1,11 +1,13 @@ from typing import Annotated, Any -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from backend.core.config import get_app_settings from backend.core.dependencies import is_logged_in +from backend.core.message_code import MessageCode from backend.db.db_setup import generate_session from backend.schemas.common import ReturnValue from backend.schemas.house import HouseCreate +from backend.schemas.house.house import HousesListResponse from backend.schemas.user import ProfileResponse from backend.services.house import HouseService @@ -16,6 +18,16 @@ settings = get_app_settings() db_dependency = Annotated[Session, Depends(generate_session)] current_user_token = Annotated[ProfileResponse, Depends(is_logged_in)] -@public_router.get("/create", response_model=ReturnValue[Any]) +@public_router.post("/create", response_model=ReturnValue[Any]) def create_house(house: HouseCreate, db: db_dependency, current_user: current_user_token) -> ReturnValue[Any]: - return ReturnValue(status=200, data="created") + try: + house_service.create(db=db, house=house) + except Exception: + raise HTTPException(status_code=400, detail=MessageCode.CREATE_HOUSE_FAIL) + return ReturnValue(status=200, data=MessageCode.CREATE_HOUSE_SUCCESS) + +@public_router.get("/all", response_model=ReturnValue[HousesListResponse]) +async def get_all_house(page: int, pageSize: int, current_user: current_user_token) -> ReturnValue[HousesListResponse]: + housesCount = house_service.get_all_count(skip=page-1, limit=pageSize) + houses = house_service.get_all(skip=page-1, limit=pageSize) + return ReturnValue(status=200, data={'total': housesCount, 'list': list(houses)}) diff --git a/backend/routes/user/user.py b/backend/routes/user/user.py index 93ec7bf..22372b2 100644 --- a/backend/routes/user/user.py +++ b/backend/routes/user/user.py @@ -23,7 +23,7 @@ def register_user(user: UserCreate, db: db_dependency) -> ReturnValue[Any]: if db_user: raise HTTPException(status_code=400, detail=MessageCode.CREATED_USER) user_service.create(db=db, user=user) - return ReturnValue(status=200, data="created") + return ReturnValue(status=200, data=MessageCode.CREATE_USER_SUCCESS) @public_router.get("/me", response_model=ReturnValue[ProfileResponse]) def get_user(current_user: current_user_token) -> ReturnValue[Any]: diff --git a/backend/schemas/house/house.py b/backend/schemas/house/house.py index bd394c2..68e71a1 100644 --- a/backend/schemas/house/house.py +++ b/backend/schemas/house/house.py @@ -1,3 +1,7 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import ConfigDict from backend.schemas.main_model import MainModel class HouseBase(MainModel): @@ -15,3 +19,14 @@ class HouseCreate(HouseBase): name: str address: str areas: list[AreaCreate] + model_config = ConfigDict(from_attributes=True) + +class HousesList(HouseCreate): + id: UUID + created_at: datetime + updated_at: datetime + deleted_at: datetime | None + +class HousesListResponse(MainModel): + total: int + list: list[HousesList] diff --git a/backend/schemas/user/user.py b/backend/schemas/user/user.py index 5a606e7..6ed3cd9 100644 --- a/backend/schemas/user/user.py +++ b/backend/schemas/user/user.py @@ -27,13 +27,12 @@ class UserProfile(MainModel): class UserSeeds(UserCreate): is_admin: bool - is_lock: bool class PrivateUser(UserBase): id: UUID name: str is_admin: bool - is_lock: bool + is_lock: datetime created_at: datetime updated_at: datetime model_config = ConfigDict(from_attributes=True) diff --git a/backend/services/house/house_service.py b/backend/services/house/house_service.py index 2022856..c5d4c13 100644 --- a/backend/services/house/house_service.py +++ b/backend/services/house/house_service.py @@ -9,3 +9,9 @@ class HouseService(BaseService): def create(self, db: Session, house: HouseCreate): return self.repos.create(db=db, house=house) + + def get_all(self, skip: int = 0, limit: int = 100): + return self.repos.get_all(skip=skip, limit=limit) + + def get_all_count(self, skip: int = 0, limit: int = 100): + return self.repos.get_count_all(skip=skip, limit=limit) diff --git a/frontend/src/api/house.js b/frontend/src/api/house.js new file mode 100644 index 0000000..4318421 --- /dev/null +++ b/frontend/src/api/house.js @@ -0,0 +1,16 @@ +import { protocol } from './index' +import { GET_HOUSES_LIST, POST_HOUSE_CREATE } from './url' + +export const postCreateHouse = (payload) => { + return protocol.post(POST_HOUSE_CREATE, payload) +} + +export const getAllHouse = ({ page, pageSize }) => { + return protocol.get( + GET_HOUSES_LIST({ + page: page(), + pageSize: pageSize(), + }), + {}, + ) +} diff --git a/frontend/src/api/url.js b/frontend/src/api/url.js index d2402e0..cbded8a 100644 --- a/frontend/src/api/url.js +++ b/frontend/src/api/url.js @@ -3,3 +3,6 @@ export const POST_LOGOUT = '/api/auth/logout' export const POST_REFRESH = '/api/auth/refresh' export const GET_USER_PROFILE = '/api/user/me' export const PUT_UPDATE_USER_PROFILE = '/api/user/update-profile' +export const POST_HOUSE_CREATE = '/api/house/create' +export const GET_HOUSES_LIST = ({ page, pageSize }) => + `/api/house/all?page=${page}&pageSize=${pageSize}` diff --git a/frontend/src/components/AreaAdd/AreaAdd.jsx b/frontend/src/components/AreaAdd/AreaAdd.jsx index 4af99e5..50ab582 100644 --- a/frontend/src/components/AreaAdd/AreaAdd.jsx +++ b/frontend/src/components/AreaAdd/AreaAdd.jsx @@ -4,7 +4,6 @@ import Textarea from '@components/common/Textarea' import { createForm } from '@felte/solid' import { validator } from '@felte/validator-yup' import useLanguage from '@hooks/useLanguage' -import useToast from '@hooks/useToast' import { IconAddressBook, IconCirclePlus, @@ -33,7 +32,6 @@ export default function AreaAdd(props) { const [openModal, setOpenModal] = createSignal(false) const [data, setData] = createSignal([]) const { language, isRequired } = useLanguage() - const notify = useToast() const { form, reset, errors } = createForm({ extend: [validator({ schema: areaSchema(language, isRequired) })], onSubmit: async (values) => { @@ -54,8 +52,6 @@ export default function AreaAdd(props) { setData((prev) => [...prev.slice(0, index), ...prev.slice(index + 1)]) } - // console.log(error()) - return (
@@ -95,7 +91,7 @@ export default function AreaAdd(props) { {(item, index) => ( diff --git a/frontend/src/components/AreaAdd/AreaItem.jsx b/frontend/src/components/AreaAdd/AreaItem.jsx index 89a8aeb..cd8a0dd 100644 --- a/frontend/src/components/AreaAdd/AreaItem.jsx +++ b/frontend/src/components/AreaAdd/AreaItem.jsx @@ -9,12 +9,12 @@ export default function AreaItem(props) {

{props.description}

diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index c252017..6909e04 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -5,6 +5,7 @@ import useAuth from '@hooks/useAuth' import useToast from '@hooks/useToast' import { A } from '@solidjs/router' import { IconLogout, IconMenuDeep, IconUserCircle } from '@tabler/icons-solidjs' +import { Helpers } from '@utils/helper' import { Show, onMount } from 'solid-js' export default function Header() { @@ -71,7 +72,7 @@ export default function Header() {
  • - + Profile diff --git a/frontend/src/components/Navbar/Navbar.jsx b/frontend/src/components/Navbar/Navbar.jsx index da0613e..28ef7eb 100644 --- a/frontend/src/components/Navbar/Navbar.jsx +++ b/frontend/src/components/Navbar/Navbar.jsx @@ -5,9 +5,10 @@ import { A } from '@solidjs/router' import { IconBuildingWarehouse, IconDashboard, - IconHome, + IconMapPin, IconTriangle, } from '@tabler/icons-solidjs' +import { Helpers } from '@utils/helper' import { For, Show, mergeProps } from 'solid-js' import { Dynamic } from 'solid-js/web' import './navbar.scss' @@ -16,19 +17,19 @@ const { language } = useLanguage() export const NAV_ITEM = (admin = false) => [ { - path: '/dashboard', + pathName: 'dashboard', show: admin, icon: IconDashboard, text: language?.ui.dashboard, }, { - path: '/house', + pathName: 'location', show: true, - icon: IconHome, + icon: IconMapPin, text: language?.ui.house, }, { - path: '/warehouse', + pathName: 'warehouse', show: true, icon: IconBuildingWarehouse, text: language?.ui.location, @@ -39,7 +40,7 @@ function NavbarItem(props) { const merged = mergeProps({ active: true }, props) return ( @@ -47,7 +48,10 @@ function NavbarItem(props) { } > - + {merged.text} @@ -126,7 +130,7 @@ export default function Navbar() {
  • - Profile + Profile
  • Logout diff --git a/frontend/src/components/common/Pagination/Pagination.jsx b/frontend/src/components/common/Pagination/Pagination.jsx new file mode 100644 index 0000000..ce090d2 --- /dev/null +++ b/frontend/src/components/common/Pagination/Pagination.jsx @@ -0,0 +1,64 @@ +import { DOTS, usePagination } from '@hooks/usePagination' +import { For, Show, splitProps } from 'solid-js' + +export default function Pagination(props) { + const [localProps, onEvents] = splitProps( + props, + ['currentPage', 'totalCount', 'pageSize'], + ['onPageChange'], + ) + + const paginationRange = usePagination(localProps) + + const onNext = () => { + onEvents.onPageChange(localProps.currentPage() + 1) + } + + const onPrevious = () => { + onEvents.onPageChange(localProps.currentPage() - 1) + } + + return ( + 1}> + + + ) +} diff --git a/frontend/src/components/common/Pagination/index.js b/frontend/src/components/common/Pagination/index.js new file mode 100644 index 0000000..8142f79 --- /dev/null +++ b/frontend/src/components/common/Pagination/index.js @@ -0,0 +1,2 @@ +export * from './Pagination' +export { default } from './Pagination' diff --git a/frontend/src/hooks/usePagination.js b/frontend/src/hooks/usePagination.js new file mode 100644 index 0000000..48d52e4 --- /dev/null +++ b/frontend/src/hooks/usePagination.js @@ -0,0 +1,60 @@ +import { createMemo } from 'solid-js' + +export const DOTS = '...' + +const range = (start, end) => { + let length = end - start + 1 + return Array.from({ length }, (_, idx) => idx + start) +} + +export const usePagination = ({ + totalCount, + pageSize, + currentPage, + siblingCount = 1, +}) => { + const paginationRange = createMemo(() => { + const totalPageCount = Math.ceil(totalCount / pageSize()) + + const totalPageNumbers = siblingCount + 5 + + if (totalPageNumbers >= totalPageCount) { + return range(1, totalPageCount) + } + + const leftSiblingIndex = Math.max(currentPage() - siblingCount, 1) + const rightSiblingIndex = Math.min( + currentPage() + siblingCount, + totalPageCount, + ) + + const shouldShowLeftDots = leftSiblingIndex > 2 + const shouldShowRightDots = rightSiblingIndex < totalPageCount - 2 + + const firstPageIndex = 1 + const lastPageIndex = totalPageCount + + if (!shouldShowLeftDots && shouldShowRightDots) { + let leftItemCount = 3 + 2 * siblingCount + let leftRange = range(1, leftItemCount) + + return [...leftRange, DOTS, totalPageCount] + } + + if (shouldShowLeftDots && !shouldShowRightDots) { + let rightItemCount = 3 + 2 * siblingCount + let rightRange = range( + totalPageCount - rightItemCount + 1, + totalPageCount, + ) + return [firstPageIndex, DOTS, ...rightRange] + } + + if (shouldShowLeftDots && shouldShowRightDots) { + let middleRange = range(leftSiblingIndex, rightSiblingIndex) + return [firstPageIndex, DOTS, ...middleRange, DOTS, lastPageIndex] + } + }) + + return paginationRange +} diff --git a/frontend/src/lang/vi.json b/frontend/src/lang/vi.json index 01b652b..ced785e 100644 --- a/frontend/src/lang/vi.json +++ b/frontend/src/lang/vi.json @@ -9,17 +9,17 @@ "changeInfo": "Thông tin tài khoản", "save": "Lưu", "clear": "Xóa", - "house": "Nhà", + "house": "Địa điểm", "action": "Thao Tác", "createNew": "Tạo mới", "location": "Nhà kho", "displayName": "Tên hiển thị", "newPassword": "Mật khẩu mới", "confirmNewPassword": "Nhập lại mật khẩu", - "newHouse": "Tạo nhà mới", - "houseName": "Tên nhà", - "houseIcon": "Ký tự nhà", - "houseAddress": "Địa chỉ nhà", + "newHouse": "Tạo địa điểm mới", + "houseName": "Tên địa điểm", + "houseIcon": "Ký tự", + "houseAddress": "Địa chỉ", "areas": "Khu vực", "areaName": "Tên khu vực", "areaDesc": "Mô tả", @@ -30,7 +30,12 @@ "confirm": "Xác nhận", "cancel": "Huỷ", "findIconHere": "Tìm ở đây", - "empty": "Trống" + "empty": "Trống", + "error": "lỗi!", + "success": "Thành công!", + "info": "Thông tin", + "loading": "Đang tải...", + "showing": "Hiển thị" }, "table": { "columnName": { @@ -42,10 +47,17 @@ } }, "message": { - "CREATED_USER": "Username already registered!", - "LOGIN_WRONG": "Your username or password input is wrong!", - "USER_LOCK": "Your Account was locked", - "IS_REQUIRED": "%s là bắt buộc", - "PASSWORD_MUSTMATCH": "Cần nhập trùng với mật khẩu" + "CREATE_USER_SUCCESS": "Tạo tài khoản thành công!", + "CREATED_USER": "Tên tài khoản đã có người sử dụng!", + "LOGIN_WRONG": "Bạn nhập sai tên người dùng hoặc mật khẩu.", + "USER_LOCK": "Tài khoản bạn hiện đang bị khóa.", + "IS_REQUIRED": "%s là bắt buộc.", + "PASSWORD_MUSTMATCH": "Cần nhập trùng với mật khẩu.", + "UPDATE_SUCCESS": "Cập nhật thành công!", + "UPDATE_PROFILE_SUCCESS": "Cập nhật hồ sơ thành công!", + "UPDATE_FAIL": "Cập nhật thất bại!", + "API_CALL_FAIL": "Call API có vẫn đề!", + "CREATE_HOUSE_FAIL": "Tạo địa điểm mới thất bại!", + "CREATE_HOUSE_SUCCESS": "Tạo địa điểm mới thành công!" } } diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index b8513b3..2af2fde 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,6 +1,7 @@ import { NAV_ITEM } from '@components/Navbar' import { useSiteContext } from '@context/SiteContext' import { useNavigate } from '@solidjs/router' +import { Helpers } from '@utils/helper' import { createEffect } from 'solid-js' function getFirstItem(array) { @@ -18,7 +19,12 @@ export default function Home() { createEffect(() => { if (store?.userInfo?.isAdmin) { const first = getFirstItem(NAV_ITEM(store?.userInfo?.isAdmin)) - navigate(first ? first.path : '/me', { replace: true }) + navigate( + first + ? Helpers.getRoutePath(first.pathName) + : Helpers.getRoutePath('profile'), + { replace: true }, + ) } }) diff --git a/frontend/src/pages/House/House.jsx b/frontend/src/pages/House/House.jsx index d91ebc0..5b2f180 100644 --- a/frontend/src/pages/House/House.jsx +++ b/frontend/src/pages/House/House.jsx @@ -1,8 +1,10 @@ import ViewSwitch, { VIEWDATA } from '@components/ViewSwitch' import useLanguage from '@hooks/useLanguage' import { IconHome } from '@tabler/icons-solidjs' -import { createSignal } from 'solid-js' +import { For, createEffect, createResource, createSignal } from 'solid-js' +import { getAllHouse } from '@api/house' +import Pagination from '@components/common/Pagination' import { A } from '@solidjs/router' import { IconHome2, @@ -13,9 +15,28 @@ import { } from '@tabler/icons-solidjs' import './house.scss' +const PAGE_SIZE = [10, 50, 100] + +const fetchHouses = async ({ page, pageSize }) => { + const response = await getAllHouse({ page, pageSize }) + return response +} + export default function House() { const { language } = useLanguage() + const [pageSize, setPageSize] = createSignal(PAGE_SIZE[0]) + const [currentPage, setCurrentPage] = createSignal(1) const [view, setView] = createSignal(VIEWDATA['list']) + const [houses] = createResource( + { page: currentPage, pageSize: pageSize }, + fetchHouses, + ) + + createEffect(() => { + if (houses()) { + console.log(houses()?.data) + } + }) const onEdit = () => { console.log('edit') @@ -25,6 +46,11 @@ export default function House() { console.log('delete') } + const onSetPageSize = (pageSize) => { + setPageSize(pageSize) + setCurrentPage(1) + } + return (
    @@ -36,7 +62,7 @@ export default function House() {
    +
  • ) } diff --git a/frontend/src/pages/HouseCreate/HouseCreate.jsx b/frontend/src/pages/HouseCreate/HouseCreate.jsx index 51970ec..b5c8ee4 100644 --- a/frontend/src/pages/HouseCreate/HouseCreate.jsx +++ b/frontend/src/pages/HouseCreate/HouseCreate.jsx @@ -1,15 +1,18 @@ +import { postCreateHouse } from '@api/house' import AreaAdd from '@components/AreaAdd' import TextInput from '@components/common/TextInput' import { createForm } from '@felte/solid' import { validator } from '@felte/validator-yup' import useLanguage from '@hooks/useLanguage' -import { A } from '@solidjs/router' +import useToast from '@hooks/useToast' +import { A, useNavigate } from '@solidjs/router' import { IconAddressBook, - IconHomePlus, IconIcons, + IconMapPinPlus, IconTag, } from '@tabler/icons-solidjs' +import { Helpers } from '@utils/helper' import * as yup from 'yup' const houseSchema = (language, isRequired) => @@ -17,15 +20,41 @@ const houseSchema = (language, isRequired) => icon: yup.string().required(isRequired(language.ui.houseIcon)), name: yup.string().required(isRequired(language.ui.houseName)), address: yup.string().required(isRequired(language.ui.houseAddress)), - areas: yup.array().required(isRequired(language.ui.areas)), + areas: yup + .array() + .min(1, isRequired(language.ui.areas)) + .required(isRequired(language.ui.areas)), }) export default function HouseCreate() { const { language, isRequired } = useLanguage() + const notify = useToast() + const navigate = useNavigate() const { form, errors } = createForm({ extend: [validator({ schema: houseSchema(language, isRequired) })], onSubmit: async (values) => { console.log(values) + const resp = await postCreateHouse(values) + return resp + }, + onSuccess: (resp) => { + if (resp.status === 200) { + notify.success({ + title: language.ui.success, + description: + language.message[resp.data] || + language.message['CREATE_HOUSE_SUCCESS'], + }) + navigate(Helpers.getRoutePath('location'), { replace: true }) + } + }, + onError: (error) => { + notify.error({ + title: language.ui.error, + description: + language.message[error.data] || language.message['API_CALL_FAIL'], + closable: true, + }) }, }) @@ -34,14 +63,14 @@ export default function HouseCreate() {
    - + {language.ui.newHouse}
    @@ -90,7 +119,13 @@ export default function HouseCreate() { />
    - +
    diff --git a/frontend/src/pages/Layout.jsx b/frontend/src/pages/Layout.jsx index 5103ff0..6814d27 100644 --- a/frontend/src/pages/Layout.jsx +++ b/frontend/src/pages/Layout.jsx @@ -21,7 +21,7 @@ export default function Layout(props) {
    -
    {props.children}
    +
    {props.children}
    diff --git a/frontend/src/pages/Profile/Profile.jsx b/frontend/src/pages/Profile/Profile.jsx index 615cbd9..ae37ac8 100644 --- a/frontend/src/pages/Profile/Profile.jsx +++ b/frontend/src/pages/Profile/Profile.jsx @@ -46,16 +46,17 @@ export default function Profile() { setUser(resp.data) reset() notify.success({ - title: 'Update profile success!', - description: 'Your profile has been updated!', + title: language.ui.success, + description: + language.message[resp.data] || + language.message['CREATE_USER_SUCCESS'], }) } } catch (error) { notify.error({ - title: 'Update profile fail!', - description: error?.data - ? language.message[error.data] - : 'Your username or password input is wrong!', + title: language.ui.error, + description: + language.message[error?.data] || language.message['API_CALL_FAIL'], closable: true, }) } diff --git a/frontend/src/routes/routes.js b/frontend/src/routes/routes.js index 7dd8926..d698c91 100644 --- a/frontend/src/routes/routes.js +++ b/frontend/src/routes/routes.js @@ -2,36 +2,42 @@ import { lazy } from 'solid-js' export const ROUTES = [ { + name: 'home', path: '/', components: lazy(() => import('@pages/Home')), filter: {}, show: true, }, { + name: 'profile', path: '/me', components: lazy(() => import('@pages/Profile')), filter: {}, show: true, }, { + name: 'dashboard', path: '/dashboard', components: lazy(() => import('@pages/Dashboard')), filter: {}, show: true, }, { - path: '/house', + name: 'location', + path: '/location', components: lazy(() => import('@pages/House')), filter: {}, show: true, }, { - path: '/house/create', + name: 'create-location', + path: '/location/create', components: lazy(() => import('@pages/HouseCreate')), filter: {}, show: true, }, { + name: 'warehouse', path: '/warehouse', components: lazy(() => import('@pages/WareHouse')), filter: {}, diff --git a/frontend/src/utils/helper.js b/frontend/src/utils/helper.js index 8656408..e05cdea 100644 --- a/frontend/src/utils/helper.js +++ b/frontend/src/utils/helper.js @@ -1,3 +1,4 @@ +import { ROUTES } from '@routes/routes' import { AES, enc } from 'crypto-js' import { LOGIN_KEY, SECRET_KEY, STORE_KEY } from './enum' @@ -58,16 +59,18 @@ export class Helpers { } static clearArrayWithNullObject = (array) => { - console.log(array) - array.forEach((element, i) => { - const obk = this.clearObject(element) - if (obk) array.splice(i, 1) - }) - // for (let i = 0; i < array.length; i++) { - // if (array[i] === null || array[i] === undefined) { - // array.splice(i, 1) - // } - // } + if (array instanceof Array) { + array.forEach((element, i) => { + if (element instanceof Object) { + const obk = this.clearObject(element) + if (obk) array.splice(i, 1) + } + }) + } + return array.length > 0 ? array : null } + + static getRoutePath = (pathName) => + ROUTES.filter((r) => r.name === pathName)[0].path } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 2845fe4..16073a1 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -6,6 +6,9 @@ export default { content: ['./src/**/*.{js,jsx}'], theme: { extend: { + screens: { + xs: '400px', + }, colors: { fu: { white: '#fff',