[CLAUDE] Phase1.2: CRUD Master + Permission Matrix + FE admin pages

Backend:
- Domain/Master: Supplier (+ SupplierType 5 loai), Project, Department (AuditableEntity)
- Domain/Identity: MenuItem, Permission, MenuKeys const (12 menu)
- EF Configurations voi unique Code + query filter IsDeleted
- DbSets + IApplicationDbContext interface update
- Application: PagedResult + PagedRequest generic
- Application/Master CQRS CRUD 3 entity (Create/Update/Delete/Get/List voi paging search sort)
- Application/Permissions: GetMyMenuTree (union OR role, filter tree), ListMenuItems, ListPermissionsByRole, UpsertPermission (guard admin khong tu giam quyen), ListRoles
- Api/Authorization: MenuPermissionRequirement + Handler (Admin bypass, query DB)
- Program.cs: register 48 policy {menu}.{action} tu MenuKeys x Actions
- Api/Controllers: Suppliers, Projects, Departments, Menus, Roles, Permissions
- DbInitializer: seed 12 menu + admin full CRUD permissions
- Migration AddMasterData + AddPermissions

Frontend (fe-admin):
- Types: menuKeys.ts const, menu.ts (MenuNode/Role/Permission), master.ts (Supplier/Project/Department + SupplierType const-object)
- AuthContext: load menu from /menus/me, cache localStorage, refreshMenu()
- usePermission hook + PermissionGuard component (wrap button)
- UI kit them: Dialog (modal overlay), Textarea, Select
- Generic: DataTable (column config, sortable, loading, empty) + Pagination
- PageHeader component
- apiError helper extract message tu ProblemDetails
- Layout rewrite: render menu dong tu AuthContext.menu (MenuGroup collapsible + NavLink + lucide icon map)
- Pages: master/Suppliers, master/Projects, master/Departments (CRUD + search + sort + paging + Dialog form)
- Page system/Permissions: ma tran Role x MenuKey x CRUD checkbox (tick tu dong PUT upsert)
- App.tsx them 4 route moi

Bug fix:
- MenuPermissionHandler: EF expression tree khong support switch expression -> tach switch ra ngoai AnyAsync
- TS erasableSyntaxOnly khong cho enum -> SupplierType const-object pattern (typeof[keyof])

E2E verified via Vite proxy:
- GET /menus/me -> 6 root + 6 child nodes (12 menus)
- GET /roles -> 12 roles
- POST/GET/PUT/DELETE /suppliers -> full CRUD, soft delete OK
- tsc -b fe-admin pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-21 11:30:14 +07:00
parent 49a5f57a50
commit 54d6c9ba52
63 changed files with 4422 additions and 93 deletions

View File

@ -1,29 +1,48 @@
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import { api, TOKEN_KEY, USER_KEY } from '@/lib/api'
import type { AuthResponse, LoginPayload, UserInfo } from '@/types/auth'
import type { MenuNode } from '@/types/menu'
type AuthContextValue = {
user: UserInfo | null
menu: MenuNode[]
isAuthenticated: boolean
isBootstrapping: boolean
login: (payload: LoginPayload) => Promise<void>
logout: () => void
refreshMenu: () => Promise<void>
}
const AuthContext = createContext<AuthContextValue | null>(null)
const MENU_KEY = 'solution-erp-admin-menu'
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<UserInfo | null>(null)
const [menu, setMenu] = useState<MenuNode[]>([])
const [isBootstrapping, setIsBootstrapping] = useState(true)
async function loadMenu() {
try {
const res = await api.get<MenuNode[]>('/menus/me')
setMenu(res.data)
localStorage.setItem(MENU_KEY, JSON.stringify(res.data))
} catch {
// keep cached menu if request fails
}
}
useEffect(() => {
const token = localStorage.getItem(TOKEN_KEY)
const raw = localStorage.getItem(USER_KEY)
if (token && raw) {
const userRaw = localStorage.getItem(USER_KEY)
const menuRaw = localStorage.getItem(MENU_KEY)
if (token && userRaw) {
try {
setUser(JSON.parse(raw))
setUser(JSON.parse(userRaw))
if (menuRaw) setMenu(JSON.parse(menuRaw))
loadMenu()
} catch {
localStorage.removeItem(USER_KEY)
localStorage.removeItem(MENU_KEY)
}
}
setIsBootstrapping(false)
@ -34,16 +53,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
localStorage.setItem(TOKEN_KEY, res.data.accessToken)
localStorage.setItem(USER_KEY, JSON.stringify(res.data.user))
setUser(res.data.user)
await loadMenu()
}
function logout() {
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(USER_KEY)
localStorage.removeItem(MENU_KEY)
setUser(null)
setMenu([])
}
return (
<AuthContext.Provider value={{ user, isAuthenticated: !!user, isBootstrapping, login, logout }}>
<AuthContext.Provider value={{ user, menu, isAuthenticated: !!user, isBootstrapping, login, logout, refreshMenu: loadMenu }}>
{children}
</AuthContext.Provider>
)