Last Commit for solidjs

This commit is contained in:
Sam Liu 2024-07-24 02:39:55 +00:00
parent 5eb89d3b99
commit 953edb3d0c
18 changed files with 397 additions and 93 deletions

View File

@ -7,3 +7,4 @@ class MessageCode():
REFRESH_TOKEN_EXPIRED: str = 'REFRESH_TOKEN_EXPIRED' REFRESH_TOKEN_EXPIRED: str = 'REFRESH_TOKEN_EXPIRED'
CREATE_HOUSE_FAIL: str = 'CREATE_HOUSE_FAIL' CREATE_HOUSE_FAIL: str = 'CREATE_HOUSE_FAIL'
CREATE_HOUSE_SUCCESS: str = 'CREATE_HOUSE_SUCCESS' CREATE_HOUSE_SUCCESS: str = 'CREATE_HOUSE_SUCCESS'
HOUSE_NOT_FOUND: str = 'HOUSE_NOT_FOUND'

View File

@ -30,4 +30,4 @@ class Areas(SqlAlchemyBase, DeleteMixin):
house: Mapped['Houses'] = relationship(back_populates="areas") house: Mapped['Houses'] = relationship(back_populates="areas")
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}, name: {self.name}" return f"<{self.__class__.__name__} id={self.id} name={self.name} house_id={self.house_id}, desc={self.desc}>"

View File

@ -1,7 +1,7 @@
from backend.db.models.houses import Houses, Areas from backend.db.models.houses import Houses, Areas
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from backend.schemas.house import HouseCreate from backend.schemas.house import HouseCreate, HouseUpdate, AreaUpdate
class RepositoryHouses: class RepositoryHouses:
def __init__(self): def __init__(self):
@ -34,3 +34,45 @@ class RepositoryHouses:
db.refresh(db_house) db.refresh(db_house)
return db_house return db_house
def update_area(self, db: Session, areas: list[AreaUpdate], house_id: str):
db_house = self.get_by_id(house_id)
if not db_house:
return None
try:
db_areaId = [area.id for area in db_house.areas]
area_update_id = [area.id for area in areas if area.id is not None]
area_delete_id = [area_id for area_id in db_areaId if area_id not in area_update_id]
if area_delete_id:
self.areas.query.where(Areas.id.in_(area_delete_id)).delete()
for area in areas:
area_obj = Areas(**area.dict(), house_id=house_id)
db.merge(area_obj)
# areaList = [dict(**area.dict(), house_id=house_id) for area in areas]
# db.execute(update(Areas), areaList)
# self.areas.query.update(areaList)
db.commit()
except Exception:
db.rollback()
raise
def update(self, db: Session, house: HouseUpdate):
db_house = self.get_by_id(house.id)
if not db_house:
return None
try:
self.houses.query.where(Houses.id == house.id).update(house.dict(exclude={"id", 'areas'}, exclude_unset=True, exclude_none=True))
db.commit()
self.update_area(db, house.areas, house.id)
except Exception:
db.rollback()
raise
db.refresh(db_house)
return db_house
def delete(self, db: Session, house_id: str):
pass

View File

@ -7,7 +7,7 @@ from backend.core.message_code import MessageCode
from backend.db.db_setup import generate_session from backend.db.db_setup import generate_session
from backend.schemas.common import ReturnValue from backend.schemas.common import ReturnValue
from backend.schemas.house import HouseCreate from backend.schemas.house import HouseCreate
from backend.schemas.house.house import HousesListResponse from backend.schemas.house.house import HouseUpdate, HousesListResponse, HouseResponse
from backend.schemas.user import ProfileResponse from backend.schemas.user import ProfileResponse
from backend.services.house import HouseService from backend.services.house import HouseService
@ -27,7 +27,19 @@ def create_house(house: HouseCreate, db: db_dependency, current_user: current_us
return ReturnValue(status=200, data=MessageCode.CREATE_HOUSE_SUCCESS) return ReturnValue(status=200, data=MessageCode.CREATE_HOUSE_SUCCESS)
@public_router.get("/all", response_model=ReturnValue[HousesListResponse]) @public_router.get("/all", response_model=ReturnValue[HousesListResponse])
async def get_all_house(page: int, pageSize: int, current_user: current_user_token) -> ReturnValue[HousesListResponse]: def get_all_house(page: int, pageSize: int, current_user: current_user_token) -> ReturnValue[HousesListResponse]:
housesCount = house_service.get_all_count() housesCount = house_service.get_all_count()
houses = house_service.get_all(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)}) return ReturnValue(status=200, data={'total': housesCount, 'list': list(houses)})
@public_router.get("/{house_id}", response_model=ReturnValue[HouseResponse])
def get_house_by_id(house_id: str, current_user: current_user_token) -> ReturnValue[HouseCreate]:
house = house_service.get_by_id(id=house_id)
if not house:
raise HTTPException(status_code=404, detail=MessageCode.HOUSE_NOT_FOUND)
return ReturnValue(status=200, data=house)
@public_router.put("/update", response_model=ReturnValue[HouseCreate])
def update_house(house: HouseUpdate, current_user: current_user_token, db: db_dependency) -> ReturnValue[Any]:
db_house = house_service.update(db=db, house=house)
return ReturnValue(status=200, data=db_house)

View File

@ -5,23 +5,33 @@ from pydantic import ConfigDict
from backend.schemas.main_model import MainModel from backend.schemas.main_model import MainModel
class HouseBase(MainModel): class HouseBase(MainModel):
pass
class AreaBase(MainModel):
pass
class AreaCreate(AreaBase):
name: str
desc: str
class HouseCreate(HouseBase):
icon: str icon: str
name: str name: str
address: str address: str
areas: list[AreaCreate]
class AreaBase(MainModel):
name: str
desc: str
class AreaUpdate(AreaBase):
id: UUID | None = None
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
class HousesList(HouseCreate): class HouseCreate(HouseBase):
areas: list[AreaBase]
model_config = ConfigDict(from_attributes=True)
class HouseUpdate(HouseBase):
id: UUID
areas: list[AreaUpdate]
model_config = ConfigDict(from_attributes=True)
class HouseResponse(HouseBase):
id: UUID
areas: list[AreaUpdate]
model_config = ConfigDict(from_attributes=True)
class HousesList(HouseBase):
id: UUID id: UUID
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime

View File

@ -1,6 +1,6 @@
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from backend.repos import RepositoryHouses from backend.repos import RepositoryHouses
from backend.schemas import HouseCreate from backend.schemas import HouseCreate, HouseUpdate
from backend.services._base_service import BaseService from backend.services._base_service import BaseService
class HouseService(BaseService): class HouseService(BaseService):
@ -13,5 +13,11 @@ class HouseService(BaseService):
def get_all(self, skip: int = 0, limit: int = 100): def get_all(self, skip: int = 0, limit: int = 100):
return self.repos.get_all(skip=skip, limit=limit) return self.repos.get_all(skip=skip, limit=limit)
def get_all_count(self, skip: int = 0, limit: int = 100): def get_all_count(self):
return self.repos.get_count_all() return self.repos.get_count_all()
def get_by_id(self, id: str):
return self.repos.get_by_id(house_id=id)
def update(self, db: Session, house: HouseUpdate):
return self.repos.update(db=db, house=house)

View File

@ -26,6 +26,7 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"solid-js": "^1.8.15", "solid-js": "^1.8.15",
"solid-toast": "^0.5.0", "solid-toast": "^0.5.0",
"uuid": "^10.0.0",
"yup": "^1.4.0" "yup": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -41,6 +41,9 @@ importers:
solid-toast: solid-toast:
specifier: ^0.5.0 specifier: ^0.5.0
version: 0.5.0(solid-js@1.8.17) version: 0.5.0(solid-js@1.8.17)
uuid:
specifier: ^10.0.0
version: 10.0.0
yup: yup:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
@ -1668,6 +1671,10 @@ packages:
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
validate-html-nesting@1.2.2: validate-html-nesting@1.2.2:
resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==}
@ -3302,6 +3309,8 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
uuid@10.0.0: {}
validate-html-nesting@1.2.2: {} validate-html-nesting@1.2.2: {}
vite-plugin-mkcert@1.17.5(vite@5.2.11(sass@1.77.4)): vite-plugin-mkcert@1.17.5(vite@5.2.11(sass@1.77.4)):

View File

@ -1,5 +1,10 @@
import { protocol } from './index' import { protocol } from './index'
import { GET_HOUSES_LIST, POST_HOUSE_CREATE } from './url' import {
GET_HOUSE_DETAIL,
GET_HOUSES_LIST,
POST_HOUSE_CREATE,
PUT_UPDATE_HOUSE,
} from './url'
export const postCreateHouse = (payload) => { export const postCreateHouse = (payload) => {
return protocol.post(POST_HOUSE_CREATE, payload) return protocol.post(POST_HOUSE_CREATE, payload)
@ -14,3 +19,11 @@ export const getAllHouse = ({ page, pageSize }) => {
{}, {},
) )
} }
export const getHouseDetail = (id) => {
return protocol.get(GET_HOUSE_DETAIL(id), {})
}
export const putUpdateHouse = (payload) => {
return protocol.put(PUT_UPDATE_HOUSE, payload)
}

View File

@ -3,6 +3,12 @@ export const POST_LOGOUT = '/api/auth/logout'
export const POST_REFRESH = '/api/auth/refresh' export const POST_REFRESH = '/api/auth/refresh'
export const GET_USER_PROFILE = '/api/user/me' export const GET_USER_PROFILE = '/api/user/me'
export const PUT_UPDATE_USER_PROFILE = '/api/user/update-profile' export const PUT_UPDATE_USER_PROFILE = '/api/user/update-profile'
/**
* House API
*/
export const POST_HOUSE_CREATE = '/api/house/create' export const POST_HOUSE_CREATE = '/api/house/create'
export const GET_HOUSES_LIST = ({ page, pageSize }) => export const GET_HOUSES_LIST = ({ page, pageSize }) =>
`/api/house/all?page=${page}&pageSize=${pageSize}` `/api/house/all?page=${page}&pageSize=${pageSize}`
export const GET_HOUSE_DETAIL = (id) => `/api/house/${id}`
export const PUT_UPDATE_HOUSE = '/api/house/update'

View File

@ -8,11 +8,20 @@ import useLanguage from '@hooks/useLanguage'
import { import {
IconAddressBook, IconAddressBook,
IconCirclePlus, IconCirclePlus,
IconEditCircle,
IconFileDescription, IconFileDescription,
IconInfoCircle, IconInfoCircle,
IconVector, IconVector,
} from '@tabler/icons-solidjs' } from '@tabler/icons-solidjs'
import { For, Show, createComponent, createSignal } from 'solid-js' import {
createComponent,
createEffect,
createSignal,
For,
Show,
untrack,
} from 'solid-js'
import { v4 as uuidv4 } from 'uuid'
import * as yup from 'yup' import * as yup from 'yup'
import AreaItem from './AreaItem' import AreaItem from './AreaItem'
@ -25,39 +34,85 @@ import AreaItem from './AreaItem'
*/ */
const areaSchema = (language, isRequired) => const areaSchema = (language, isRequired) =>
yup.object({ yup.object({
name: yup.string().required(isRequired(language.ui.areaName)), name: yup
description: yup.string().required(isRequired(language.ui.areaName)), .string()
.min(3, language.message['MIN_THREE_CHAR'])
.required(isRequired(language.ui.areaName)),
desc: yup
.string()
.min(3, language.message['MIN_THREE_CHAR'])
.required(isRequired(language.ui.areaName)),
}) })
export default function AreaAdd(props) { export default function AreaAdd(props) {
const [openModal, setOpenModal] = createSignal(false) const [openModal, setOpenModal] = createSignal(false)
const [data, setData] = createSignal([]) const [dataArea, setDataArea] = createSignal([])
const [editMode, setEditMode] = createSignal(false)
const { language, isRequired } = useLanguage() const { language, isRequired } = useLanguage()
const { form, reset, errors } = createForm({ const { form, reset, data, setData, errors, createSubmitHandler } =
createForm({
extend: [validator({ schema: areaSchema(language, isRequired) })], extend: [validator({ schema: areaSchema(language, isRequired) })],
onSubmit: async (values) => { onSubmit: async (values) => {
setData((prev) => [...prev, values]) values.id = uuidv4()
values.isCreate = true
setDataArea((prev) => [...prev, values])
onModalClose() onModalClose()
}, },
}) })
createEffect(() => {
if (props.value && dataArea().length === 0) {
console.log(props.value)
untrack(() => setDataArea(props.value))
}
})
const onModalClose = () => { const onModalClose = () => {
setOpenModal(false), reset() setOpenModal(false), reset(), setEditMode(false)
} }
const onOpenModal = () => { const onOpenModal = () => {
setOpenModal(true) setOpenModal(true)
} }
const onConfirmDelete = (index) => { const onConfirmDelete = (delId) => {
setData((prev) => [...prev.slice(0, index), ...prev.slice(index + 1)]) setDataArea((prev) => {
prev = prev.filter((item) => item.id !== delId)
return [...prev]
})
props.setData('areas', dataArea())
} }
const onClickDeleteItem = (index) => { const onClickEditItem = (id) => {
const editItem = dataArea().find((item) => item.id === id)
setData(editItem)
setEditMode(true)
setOpenModal(true)
}
const onClickUpdateItem = createSubmitHandler({
onSubmit: (values) => {
setDataArea((prev) => {
prev = prev.map((item) => {
if (item.id === values.id) {
return values
}
return item
})
return [...prev]
})
props.setData('areas', dataArea())
reset()
setEditMode(false)
onModalClose()
},
})
const onClickDeleteItem = (delId) => {
createComponent(ConfirmPopup, { createComponent(ConfirmPopup, {
title: language?.message['CONFIRM_DELETE'], title: language?.message['CONFIRM_DELETE'],
children: language?.message['CONFIRM_DELETE_NOTE'], children: language?.message['CONFIRM_DELETE_NOTE'],
deleteId: index, deleteId: delId,
onConfirm: onConfirmDelete, onConfirm: onConfirmDelete,
}) })
} }
@ -96,14 +151,15 @@ export default function AreaAdd(props) {
classList={{ 'border-red-500': props.error }} classList={{ 'border-red-500': props.error }}
> >
<div class="p-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div class="p-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Show when={data().length > 0} fallback={language.ui.empty}> <Show when={dataArea().length > 0} fallback={language.ui.empty}>
<For each={data()}> <For each={dataArea()}>
{(item, index) => ( {(item, index) => (
<AreaItem <AreaItem
{...item} data={item}
formName={props.name} formName={props.name}
key={index()} num={index()}
onDelete={onClickDeleteItem} onDelete={onClickDeleteItem}
onEdit={onClickEditItem}
/> />
)} )}
</For> </For>
@ -112,8 +168,14 @@ export default function AreaAdd(props) {
</div> </div>
</div> </div>
<Popup <Popup
icon={<IconCirclePlus size={20} class="text-green-500" />} icon={
title={language.ui.addArea} editMode() ? (
<IconEditCircle size={20} class="text-blue-500" />
) : (
<IconCirclePlus size={20} class="text-green-500" />
)
}
title={editMode() ? language.ui.editArea : language.ui.addArea}
titleClass="text-lg" titleClass="text-lg"
openModal={openModal()} openModal={openModal()}
onModalClose={onModalClose} onModalClose={onModalClose}
@ -126,20 +188,35 @@ export default function AreaAdd(props) {
name="name" name="name"
label={language.ui.areaName} label={language.ui.areaName}
placeholder={language.ui.areaName} placeholder={language.ui.areaName}
value={data('name')}
error={errors('name')} error={errors('name')}
/> />
<Textarea <Textarea
icon={IconFileDescription} icon={IconFileDescription}
name="description" name="desc"
label={language.ui.areaDesc} label={language.ui.areaDesc}
placeholder={language.ui.areaDesc} placeholder={language.ui.areaDesc}
error={errors('description')} value={data('desc')}
error={errors('desc')}
/> />
</div> </div>
<div class="modal-action"> <div class="modal-action">
<Show
when={editMode()}
fallback={
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
{language.ui.save} {language.ui.save}
</button> </button>
}
>
<button
type="submit"
class="btn btn-primary"
onClick={onClickUpdateItem}
>
{language.ui.update}
</button>
</Show>
<button type="button" class="btn btn-ghost" onClick={onModalClose}> <button type="button" class="btn btn-ghost" onClick={onModalClose}>
{language.ui.cancel} {language.ui.cancel}
</button> </button>

View File

@ -1,32 +1,54 @@
import { IconTrash } from '@tabler/icons-solidjs' import { IconPencil, IconTrash } from '@tabler/icons-solidjs'
import { Show } from 'solid-js' import { Show } from 'solid-js'
export default function AreaItem(props) { export default function AreaItem(props) {
const data = () => props.data
return ( return (
<div class="flex justify-between items-center shadow rounded-lg p-3 border border-gray-300"> <div class="flex justify-between shadow rounded-lg p-3 border border-gray-300">
<div class=""> <div class="item-body">
<span class="text-md font-bold">{props.name}</span> <span class="text-md font-bold">{data().name}</span>
<p class="text-xs">{props.description}</p> <p class="text-xs">{data().desc}</p>
<Show when={!data().isCreate}>
<input <input
type="hidden" type="hidden"
name={`${props.formName}.${props.key}.name`} name={`${props.formName}.${props.num}.id`}
value={props.name} value={data().id}
/>
</Show>
<input
type="hidden"
name={`${props.formName}.${props.num}.name`}
value={data().name}
data-value={data().name}
/> />
<input <input
type="hidden" type="hidden"
name={`${props.formName}.${props.key}.desc`} name={`${props.formName}.${props.num}.desc`}
value={props.description} value={data().desc}
data-value={data().desc}
/> />
</div> </div>
<div class="flex">
<Show when={props.onEdit}>
<button
type="button"
class="btn btn-circle btn-ghost btn-sm text-blue-500 hover:bg-blue-100"
onClick={() => props.onEdit(data().id)}
>
<IconPencil size={20} />
</button>
</Show>
<Show when={props.onDelete}> <Show when={props.onDelete}>
<button <button
type="button" type="button"
class="btn btn-circle btn-ghost btn-sm text-red-500 hover:bg-red-100" class="btn btn-circle btn-ghost btn-sm text-red-500 hover:bg-red-100"
onClick={() => props.onDelete(props.key)} onClick={() => props.onDelete(data().id)}
> >
<IconTrash size={20} /> <IconTrash size={20} />
</button> </button>
</Show> </Show>
</div> </div>
</div>
) )
} }

View File

@ -17,6 +17,7 @@
"newPassword": "Mật khẩu mới", "newPassword": "Mật khẩu mới",
"confirmNewPassword": "Nhập lại mật khẩu", "confirmNewPassword": "Nhập lại mật khẩu",
"newHouse": "Tạo địa điểm mới", "newHouse": "Tạo địa điểm mới",
"editHouse": "Sửa địa điểm",
"houseName": "Tên địa điểm", "houseName": "Tên địa điểm",
"houseIcon": "Ký tự", "houseIcon": "Ký tự",
"houseAddress": "Địa chỉ", "houseAddress": "Địa chỉ",
@ -24,6 +25,7 @@
"areaName": "Tên khu vực", "areaName": "Tên khu vực",
"areaDesc": "Mô tả", "areaDesc": "Mô tả",
"addArea": "Thêm khu vực", "addArea": "Thêm khu vực",
"editArea": "Sửa khu vực",
"create": "Tạo", "create": "Tạo",
"update": "Cập nhật", "update": "Cập nhật",
"delete": "Xóa", "delete": "Xóa",
@ -60,6 +62,7 @@
"CREATE_HOUSE_FAIL": "Tạo địa điểm mới thất bại!", "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!", "CREATE_HOUSE_SUCCESS": "Tạo địa điểm mới thành công!",
"CONFIRM_DELETE": "Bạn có chắc là muốn xóa mục này?", "CONFIRM_DELETE": "Bạn có chắc là muốn xóa mục này?",
"CONFIRM_DELETE_NOTE": "Chú Ý ‼: Một khi đã XÓA thì không thể nào khôi phục lại được." "CONFIRM_DELETE_NOTE": "Chú Ý ‼: Một khi đã XÓA thì không thể nào khôi phục lại được.",
"MIN_THREE_CHAR": "Ít nhất phải có 3 ký tự"
} }
} }

View File

@ -1,17 +1,25 @@
import ViewSwitch, { VIEWDATA } from '@components/ViewSwitch' import ViewSwitch, { VIEWDATA } from '@components/ViewSwitch'
import useLanguage from '@hooks/useLanguage' import useLanguage from '@hooks/useLanguage'
import * as icons from '@tabler/icons-solidjs' import * as icons from '@tabler/icons-solidjs'
import { For, createEffect, createResource, createSignal } from 'solid-js' import {
For,
createComponent,
createEffect,
createResource,
createSignal,
} from 'solid-js'
import { getAllHouse } from '@api/house' import { getAllHouse } from '@api/house'
import ConfirmPopup from '@components/common/ConfirmPopup'
import Pagination from '@components/common/Pagination' import Pagination from '@components/common/Pagination'
import { A } from '@solidjs/router' import { A, useNavigate } from '@solidjs/router'
import { import {
IconHome, IconHome,
IconPencil, IconPencil,
IconSquareRoundedPlus, IconSquareRoundedPlus,
IconTrash, IconTrash,
} from '@tabler/icons-solidjs' } from '@tabler/icons-solidjs'
import { Helpers } from '@utils/helper'
import { Dynamic } from 'solid-js/web' import { Dynamic } from 'solid-js/web'
import './house.scss' import './house.scss'
@ -31,6 +39,7 @@ const fetchHouses = async ({ page, pageSize }) => {
export default function House() { export default function House() {
const { language } = useLanguage() const { language } = useLanguage()
const navigate = useNavigate()
const [pageSize, setPageSize] = createSignal(PAGE_SIZE[0]) const [pageSize, setPageSize] = createSignal(PAGE_SIZE[0])
const [currentPage, setCurrentPage] = createSignal(1) const [currentPage, setCurrentPage] = createSignal(1)
const [view, setView] = createSignal(VIEWDATA['list']) const [view, setView] = createSignal(VIEWDATA['list'])
@ -48,12 +57,23 @@ export default function House() {
} }
}) })
const onEdit = () => { const onClickEdit = (editId) => {
console.log('edit') navigate(Helpers.getRoutePath('edit-location').replace(':id', editId), {
replace: true,
})
} }
const onDelete = () => { const onConfirmDelete = (deleteId) => {
console.log('delete') console.log(deleteId)
}
const onClickDelete = (deleteId) => {
createComponent(ConfirmPopup, {
title: language?.message['CONFIRM_DELETE'],
children: language?.message['CONFIRM_DELETE_NOTE'],
deleteId,
onConfirm: onConfirmDelete,
})
} }
const onSetPageSize = (pageSize) => { const onSetPageSize = (pageSize) => {
@ -100,21 +120,21 @@ export default function House() {
<div class="col hide w-1/12"> <div class="col hide w-1/12">
{pageSize() * currentPage() - (pageSize() - idx() - 1)} {pageSize() * currentPage() - (pageSize() - idx() - 1)}
</div> </div>
<div class="col w-1/12"> <div class="col symbol w-1/12">
<Dynamic component={icons[item?.icon]} size={21} /> <Dynamic component={icons[item?.icon]} class="w-7 h-7" />
</div> </div>
<div class="col w-4/12">{item?.name}</div> <div class="col w-4/12">{item?.name}</div>
<div class="col w-4/12">{item?.address}</div> <div class="col w-4/12">{item?.address}</div>
<div class="col actionbar w-2/12"> <div class="col actionbar w-2/12">
<button <button
class="btn btn-ghost btn-sm px-1 text-blue-500 mr-2" class="btn btn-ghost btn-sm px-1 text-blue-500"
onClick={onEdit} onClick={[onClickEdit, item?.id]}
> >
<IconPencil size={20} /> <IconPencil size={20} />
</button> </button>
<button <button
class="btn btn-ghost btn-sm px-1 text-red-500" class="btn btn-ghost btn-sm px-1 text-red-500"
onClick={onDelete} onClick={[onClickDelete, item?.id]}
> >
<IconTrash size={20} /> <IconTrash size={20} />
</button> </button>
@ -144,7 +164,7 @@ export default function House() {
tabindex="0" tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-32 p-2 shadow mb-2" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-32 p-2 shadow mb-2"
> >
<For each={PAGE_SIZE.reverse()}> <For each={PAGE_SIZE}>
{(pageSize) => ( {(pageSize) => (
<li> <li>
<a onClick={[onSetPageSize, pageSize]}>{pageSize}</a> <a onClick={[onSetPageSize, pageSize]}>{pageSize}</a>

View File

@ -12,6 +12,10 @@
&.view-head { &.view-head {
@apply bg-base-200 font-bold; @apply bg-base-200 font-bold;
} }
& > .actionbar {
@apply flex gap-2;
}
} }
} }
@ -24,10 +28,11 @@
& .row { & .row {
&.view-item { &.view-item {
@apply card bg-base-100 shadow-md border border-gray-200 p-3 relative pb-8; @apply card bg-base-100 shadow-md border border-gray-200 relative
grid grid-rows-2 grid-flow-col p-2 pr-4 gap-1 auto-cols-max overflow-hidden;
& .col { & .col {
@apply w-auto @apply w-auto;
} }
& > .hide { & > .hide {
@ -35,10 +40,23 @@
visibility: hidden; visibility: hidden;
} }
& .col.symbol {
@apply row-span-2 border-r border-gray-300 bg-white flex items-center pr-2 mr-2;
& > svg {
@apply w-14 h-14;
}
}
& > .actionbar { & > .actionbar {
@apply absolute right-0 bottom-0 flex px-3 py-1 w-auto border-t border-l rounded-tl-xl; @apply absolute flex flex-col justify-center px-3 w-auto h-full border-l rounded-xl bg-white z-10 transition-all;
right: -45px;
box-shadow: -1px -1px 10px 0px rgba(0,0,0,0.1); box-shadow: -1px -1px 10px 0px rgba(0,0,0,0.1);
} }
&:hover > .actionbar {
@apply right-0;
}
} }
&.view-head { &.view-head {

View File

@ -1,40 +1,77 @@
import { postCreateHouse } from '@api/house' import { getHouseDetail, postCreateHouse, putUpdateHouse } from '@api/house'
import AreaAdd from '@components/AreaAdd' import AreaAdd from '@components/AreaAdd'
import TextInput from '@components/common/TextInput' import TextInput from '@components/common/TextInput'
import { createForm } from '@felte/solid' import { createForm } from '@felte/solid'
import { validator } from '@felte/validator-yup' import { validator } from '@felte/validator-yup'
import useLanguage from '@hooks/useLanguage' import useLanguage from '@hooks/useLanguage'
import useToast from '@hooks/useToast' import useToast from '@hooks/useToast'
import { A, useNavigate } from '@solidjs/router' import { A, useNavigate, useParams } from '@solidjs/router'
import { import {
IconAddressBook, IconAddressBook,
IconIcons, IconIcons,
IconLocationBolt,
IconMapPinPlus, IconMapPinPlus,
IconTag, IconTag,
} from '@tabler/icons-solidjs' } from '@tabler/icons-solidjs'
import { Helpers } from '@utils/helper' import { Helpers } from '@utils/helper'
import { createEffect, createResource, Show } from 'solid-js'
import * as yup from 'yup' import * as yup from 'yup'
const houseSchema = (language, isRequired) => const houseSchema = (language, isRequired) =>
yup.object({ yup.object({
icon: yup.string().required(isRequired(language.ui.houseIcon)), icon: yup
name: yup.string().required(isRequired(language.ui.houseName)), .string()
address: yup.string().required(isRequired(language.ui.houseAddress)), .min(3, language.message['MIN_THREE_CHAR'])
.required(isRequired(language.ui.houseIcon)),
name: yup
.string()
.min(3, language.message['MIN_THREE_CHAR'])
.required(isRequired(language.ui.houseName)),
address: yup
.string()
.min(3, language.message['MIN_THREE_CHAR'])
.required(isRequired(language.ui.houseAddress)),
areas: yup areas: yup
.array() .array()
.min(1, isRequired(language.ui.areas)) .min(1, isRequired(language.ui.areas))
.required(isRequired(language.ui.areas)), .required(isRequired(language.ui.areas)),
}) })
const fetchHouses = async (id) => {
if (id) {
const response = await getHouseDetail(id)
if (response.status === 200) {
return response.data
}
return response
}
return null
}
export default function HouseCreate() { export default function HouseCreate() {
const { language, isRequired } = useLanguage() const { language, isRequired } = useLanguage()
const notify = useToast() const notify = useToast()
const navigate = useNavigate() const navigate = useNavigate()
const { form, errors } = createForm({ const { id } = useParams()
const [house] = createResource(id, fetchHouses)
const { form, data, setData, errors } = createForm({
extend: [validator({ schema: houseSchema(language, isRequired) })], extend: [validator({ schema: houseSchema(language, isRequired) })],
onSubmit: async (values) => { onSubmit: async (values) => {
console.log(values) const call = id ? putUpdateHouse : postCreateHouse
const resp = await postCreateHouse(values) const result = {
...values,
areas: values.areas.map((item) =>
id
? { ...item }
: {
name: item.name,
desc: item.desc,
},
),
}
console.log(values.areas)
const resp = await call(result)
return resp return resp
}, },
onSuccess: (resp) => { onSuccess: (resp) => {
@ -45,7 +82,7 @@ export default function HouseCreate() {
language.message[resp.data] || language.message[resp.data] ||
language.message['CREATE_HOUSE_SUCCESS'], language.message['CREATE_HOUSE_SUCCESS'],
}) })
navigate(Helpers.getRoutePath('location'), { replace: true }) // navigate(Helpers.getRoutePath('location'), { replace: true })
} }
}, },
onError: (error) => { onError: (error) => {
@ -58,6 +95,12 @@ export default function HouseCreate() {
}, },
}) })
createEffect(() => {
if (id && house()) {
setData(house())
}
})
return ( return (
<div class="house-create"> <div class="house-create">
<div class="text-sm breadcrumbs mb-2"> <div class="text-sm breadcrumbs mb-2">
@ -65,18 +108,21 @@ export default function HouseCreate() {
<li> <li>
<A href={Helpers.getRoutePath('location')}>{language.ui.house}</A> <A href={Helpers.getRoutePath('location')}>{language.ui.house}</A>
</li> </li>
<li>{language.ui.newHouse}</li> <li>{id ? language.ui.editHouse : language.ui.newHouse}</li>
</ul> </ul>
</div> </div>
<div class="flex items-center gap-2 mb-5 text-xl"> <div class="flex items-center gap-2 mb-5 text-xl">
<span class="text-secondary"> <span class="text-secondary">
<IconMapPinPlus size={30} /> {id ? <IconLocationBolt size={30} /> : <IconMapPinPlus size={30} />}
</span> </span>
{language.ui.newHouse} {id ? language.ui.editHouse : language.ui.newHouse}
</div> </div>
<div class="card w-full bg-base-100 shadow-lg border border-gray-200"> <div class="card w-full bg-base-100 shadow-lg border border-gray-200">
<div class="card-body"> <div class="card-body">
<form autoComplete="off" use:form> <form autoComplete="off" use:form>
<Show when={id}>
<input type="hidden" name="id" value={id} />
</Show>
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2"> <div class="grid gap-4 grid-cols-1 lg:grid-cols-2">
<div class="col-auto"> <div class="col-auto">
<TextInput <TextInput
@ -85,6 +131,7 @@ export default function HouseCreate() {
label={language.ui.houseIcon} label={language.ui.houseIcon}
labelClass="md:w-40" labelClass="md:w-40"
placeholder={language.ui.houseIcon} placeholder={language.ui.houseIcon}
value={data('icon')}
error={errors('icon')} error={errors('icon')}
> >
<div class="label"> <div class="label">
@ -105,6 +152,7 @@ export default function HouseCreate() {
label={language.ui.houseName} label={language.ui.houseName}
labelClass="md:w-40" labelClass="md:w-40"
placeholder={language.ui.houseName} placeholder={language.ui.houseName}
value={data('name')}
error={errors('name')} error={errors('name')}
/> />
</div> </div>
@ -115,12 +163,15 @@ export default function HouseCreate() {
label={language.ui.houseAddress} label={language.ui.houseAddress}
labelClass="md:w-40" labelClass="md:w-40"
placeholder={language.ui.houseAddress} placeholder={language.ui.houseAddress}
value={data('address')}
error={errors('address')} error={errors('address')}
/> />
</div> </div>
<div class="col-auto lg:col-span-2"> <div class="col-auto lg:col-span-2">
<AreaAdd <AreaAdd
name="areas" name="areas"
value={data('areas')}
setData={setData}
error={ error={
errors('areas') && errors('areas') &&
Helpers.clearArrayWithNullObject(errors('areas')) Helpers.clearArrayWithNullObject(errors('areas'))
@ -130,8 +181,11 @@ export default function HouseCreate() {
</div> </div>
<div class="card-actions"> <div class="card-actions">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
{language.ui.create} {id ? language.ui.update : language.ui.create}
</button> </button>
<A href={Helpers.getRoutePath('location')} class="btn btn-ghost">
{language.ui.cancel}
</A>
</div> </div>
</form> </form>
</div> </div>

View File

@ -36,6 +36,15 @@ export const ROUTES = [
filter: {}, filter: {},
show: true, show: true,
}, },
{
name: 'edit-location',
path: '/location/edit/:id',
components: lazy(() => import('@pages/HouseCreate')),
filter: {
id: /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
},
show: true,
},
{ {
name: 'warehouse', name: 'warehouse',
path: '/warehouse', path: '/warehouse',

View File

@ -31,7 +31,7 @@ export default defineConfig(({ mode }) => {
plugins: [solid()], plugins: [solid()],
server: { server: {
https: false, https: false,
host: true, host: false,
port: 5001, port: 5001,
strictPort: true, strictPort: true,
watch: { watch: {
@ -59,6 +59,7 @@ export default defineConfig(({ mode }) => {
// plugins: [solid(), mkcert()], // plugins: [solid(), mkcert()],
plugins: [solid()], plugins: [solid()],
server: { server: {
open: true,
https: false, https: false,
host: true, host: true,
port: 5001, port: 5001,