vtth
This commit is contained in:
765
UI_DESIGN.md
Normal file
765
UI_DESIGN.md
Normal file
@@ -0,0 +1,765 @@
|
|||||||
|
# Portable UI Style Guide - AccManager Inspired
|
||||||
|
|
||||||
|
Tài liệu này dùng để mang phong cách UI hiện tại của AccManager sang một dự án khác. Nội dung tập trung vào ngôn ngữ thiết kế, token, layout, component pattern và checklist triển khai, không phụ thuộc chặt vào nghiệp vụ tài khoản/tài sản của AccManager.
|
||||||
|
|
||||||
|
## 1. Design DNA
|
||||||
|
|
||||||
|
Phong cách UI gốc là một **admin console nội bộ**: gọn, sáng, nhiều dữ liệu, dễ quét bảng, thao tác nhanh và ít trang trí thừa.
|
||||||
|
|
||||||
|
Tinh thần chính:
|
||||||
|
|
||||||
|
- Ưu tiên hiệu quả vận hành hơn cảm giác marketing.
|
||||||
|
- Nền sáng, bề mặt trắng/xám nhạt, màu nhấn xanh primary.
|
||||||
|
- Layout chắc, dày thông tin nhưng vẫn thoáng.
|
||||||
|
- Component nhỏ gọn: table, filter, modal, form, badge, icon action.
|
||||||
|
- Button và icon rõ chức năng, phản hồi hover/active nhẹ.
|
||||||
|
- Typography dùng heading đậm, label nhỏ uppercase, nội dung bảng vừa đủ đọc.
|
||||||
|
- Phù hợp cho dashboard, CRM, ERP mini, tool nội bộ, quản lý dữ liệu, asset/account/user management.
|
||||||
|
|
||||||
|
Không nên biến phong cách này thành landing page, hero lớn, nhiều gradient trang trí, card marketing hoặc layout quá thoáng kiểu portfolio.
|
||||||
|
|
||||||
|
## 2. Stack Khuyến Nghị
|
||||||
|
|
||||||
|
Để tái tạo đúng style, dự án mới nên dùng:
|
||||||
|
|
||||||
|
- Tailwind CSS.
|
||||||
|
- Font heading: `Manrope`.
|
||||||
|
- Font body: `Inter`.
|
||||||
|
- Icon: Google Material Symbols Outlined.
|
||||||
|
- Toast/notification: Notiflix hoặc thư viện tương đương.
|
||||||
|
- Table data với sticky header, horizontal scroll trên mobile.
|
||||||
|
|
||||||
|
Google Fonts:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
Material Symbols base CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.material-symbols-outlined {
|
||||||
|
font-family: 'Material Symbols Outlined';
|
||||||
|
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: none;
|
||||||
|
display: inline-flex;
|
||||||
|
white-space: nowrap;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Tailwind Design Tokens
|
||||||
|
|
||||||
|
Có thể copy phần `theme.extend` này sang `tailwind.config.js` của dự án mới.
|
||||||
|
|
||||||
|
```js
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
"primary": "#3755c3",
|
||||||
|
"primary-dim": "#2848b7",
|
||||||
|
"primary-container": "#dde1ff",
|
||||||
|
"on-primary": "#f8f7ff",
|
||||||
|
"background": "#f7f9fb",
|
||||||
|
"surface": "#f7f9fb",
|
||||||
|
"surface-container-lowest": "#ffffff",
|
||||||
|
"surface-container-low": "#f0f4f7",
|
||||||
|
"surface-container": "#e8eff3",
|
||||||
|
"surface-container-high": "#e1e9ee",
|
||||||
|
"surface-container-highest": "#d9e4ea",
|
||||||
|
"on-surface": "#2a3439",
|
||||||
|
"on-surface-variant": "#566166",
|
||||||
|
"outline": "#717c82",
|
||||||
|
"outline-variant": "#a9b4b9",
|
||||||
|
"error": "#9f403d",
|
||||||
|
"error-container": "#fe8983"
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
headline: ["Manrope", "sans-serif"],
|
||||||
|
body: ["Inter", "sans-serif"],
|
||||||
|
label: ["Inter", "sans-serif"]
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
DEFAULT: "0.125rem",
|
||||||
|
lg: "0.25rem",
|
||||||
|
xl: "0.5rem",
|
||||||
|
full: "0.75rem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Base body:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<body class="bg-background text-on-surface antialiased">
|
||||||
|
```
|
||||||
|
|
||||||
|
Font rule:
|
||||||
|
|
||||||
|
```css
|
||||||
|
body { font-family: 'Inter', sans-serif; }
|
||||||
|
h1, h2, h3, .brand-logo { font-family: 'Manrope', sans-serif; }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Color Usage
|
||||||
|
|
||||||
|
Màu nền:
|
||||||
|
|
||||||
|
- App background: `bg-background`.
|
||||||
|
- Main card/table: `bg-white` hoặc `bg-surface-container-lowest`.
|
||||||
|
- Filter bar/input subtle: `bg-surface-container-low`.
|
||||||
|
- Header/table head: `bg-slate-50`.
|
||||||
|
|
||||||
|
Màu chữ:
|
||||||
|
|
||||||
|
- Text chính: `text-on-surface`, `text-slate-900`.
|
||||||
|
- Text phụ: `text-on-surface-variant`, `text-slate-500`, `text-slate-600`.
|
||||||
|
- Link/action chính: `text-primary`.
|
||||||
|
|
||||||
|
Màu hành động:
|
||||||
|
|
||||||
|
- Primary: `bg-primary hover:bg-primary-dim text-on-primary`.
|
||||||
|
- Secondary: border slate, text slate.
|
||||||
|
- Danger: `bg-red-600 hover:bg-red-700 text-white` hoặc `text-error`.
|
||||||
|
- Success: emerald/green.
|
||||||
|
- Pending/warning: amber/yellow.
|
||||||
|
- Info: blue.
|
||||||
|
|
||||||
|
Tránh:
|
||||||
|
|
||||||
|
- Dùng quá nhiều gradient.
|
||||||
|
- Dùng một palette đơn sắc toàn xanh.
|
||||||
|
- Nền tối toàn trang nếu chưa thiết kế dark mode đầy đủ.
|
||||||
|
- Màu quá rực trên bảng dữ liệu.
|
||||||
|
|
||||||
|
## 5. Typography Scale
|
||||||
|
|
||||||
|
Page title:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight">Page Title</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
Subtitle:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p class="text-sm text-on-surface-variant">Short page description.</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
Table header / form label:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="text-[10px] font-bold uppercase tracking-wider text-on-surface-variant">Label</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
Table body:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">Value</td>
|
||||||
|
```
|
||||||
|
|
||||||
|
Badge text:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="px-2 py-1 rounded text-xs font-semibold">Status</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
Nguyên tắc:
|
||||||
|
|
||||||
|
- Heading đậm, ngắn, dễ scan.
|
||||||
|
- Label nhỏ, uppercase, tracking rộng.
|
||||||
|
- Nội dung bảng không quá lớn để giữ mật độ dữ liệu.
|
||||||
|
- Không dùng hero-scale type trong dashboard/tool view.
|
||||||
|
|
||||||
|
## 6. App Shell Pattern
|
||||||
|
|
||||||
|
Dùng shell này cho các app quản trị/dữ liệu.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<body class="app-shell bg-background text-on-surface antialiased flex h-screen w-screen overflow-hidden">
|
||||||
|
<aside id="appSidebar" class="h-screen w-56 flex flex-col bg-slate-100 border-r border-outline-variant/10 shrink-0">
|
||||||
|
<!-- Sidebar content -->
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<button id="sidebarBackdrop" type="button" aria-label="Close navigation"></button>
|
||||||
|
|
||||||
|
<main id="appMain" class="flex-1 flex flex-col h-screen min-w-0">
|
||||||
|
<header class="h-14 flex items-center justify-between px-6 bg-slate-50/80 backdrop-blur-xl border-b border-outline-variant/10 shrink-0">
|
||||||
|
<!-- Topbar actions -->
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="mainContent" class="flex-1 overflow-hidden">
|
||||||
|
<!-- Current page -->
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
Sidebar style:
|
||||||
|
|
||||||
|
- Width desktop: `w-56`.
|
||||||
|
- Background: `bg-slate-100`.
|
||||||
|
- Border right: `border-outline-variant/10`.
|
||||||
|
- Group label: uppercase, `text-[10px]`, tracking rộng.
|
||||||
|
- Active nav: left border xanh, nền slate nhạt, text đậm.
|
||||||
|
- Nav item: icon + label, height vừa phải, hover nền slate.
|
||||||
|
|
||||||
|
Active nav example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a class="flex items-center gap-3 px-3 py-2 border-l-4 border-blue-600 bg-slate-200/80 text-slate-900 font-bold rounded-r-lg">
|
||||||
|
<span class="material-symbols-outlined">dashboard</span>
|
||||||
|
<span>Tổng quan</span>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
Inactive nav:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a class="flex items-center gap-3 px-3 py-2 text-slate-600 hover:text-slate-900 hover:bg-slate-200/50 transition-all rounded-r-lg">
|
||||||
|
<span class="material-symbols-outlined">apps</span>
|
||||||
|
<span>Ứng dụng</span>
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Page Layout Pattern
|
||||||
|
|
||||||
|
Mỗi page nên theo cấu trúc:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="page-name flex flex-col p-4 md:p-6 overflow-hidden h-full">
|
||||||
|
<div class="page-header flex items-center justify-between gap-4 mb-5 shrink-0">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight">Page Title</h1>
|
||||||
|
<p class="text-sm text-on-surface-variant">Short explanation.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<!-- Action buttons -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-filters flex items-center gap-3 mb-4 shrink-0">
|
||||||
|
<!-- Search/filter controls -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
|
||||||
|
<!-- Table/list/content -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Nguyên tắc:
|
||||||
|
|
||||||
|
- Page padding luôn `p-4 md:p-6`.
|
||||||
|
- Header không quá cao.
|
||||||
|
- Action chính nằm bên phải.
|
||||||
|
- Filter nằm ngay trên bảng.
|
||||||
|
- Content panel chiếm hết phần còn lại.
|
||||||
|
- Không bọc section bằng quá nhiều card lồng nhau.
|
||||||
|
|
||||||
|
## 8. Dashboard Pattern
|
||||||
|
|
||||||
|
Dashboard nên có:
|
||||||
|
|
||||||
|
- Header ngắn.
|
||||||
|
- CTA chính.
|
||||||
|
- Metric grid.
|
||||||
|
- Recent activity hoặc bảng tóm tắt.
|
||||||
|
|
||||||
|
Metric card:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col">
|
||||||
|
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Applications</span>
|
||||||
|
<div class="flex items-baseline justify-between">
|
||||||
|
<span class="text-2xl font-black text-on-surface">24</span>
|
||||||
|
<span class="text-[10px] font-bold text-on-surface-variant">18 Active</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Grid:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="dashboard-stats grid grid-cols-4 gap-4 shrink-0">
|
||||||
|
```
|
||||||
|
|
||||||
|
Responsive:
|
||||||
|
|
||||||
|
- Desktop: 4 cột.
|
||||||
|
- Tablet/mobile: 2 cột.
|
||||||
|
- Mobile nhỏ: 1 cột.
|
||||||
|
|
||||||
|
## 9. Table Pattern
|
||||||
|
|
||||||
|
Đây là component quan trọng nhất của style này.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
|
||||||
|
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1">
|
||||||
|
<table class="w-full text-left border-collapse" style="min-width: 900px;">
|
||||||
|
<thead class="sticky top-0 z-10 bg-slate-50 border-b border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Name</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500 text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
<tr class="hover:bg-slate-50/80 transition-colors">
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-700">Example</td>
|
||||||
|
<td class="px-4 py-3 text-right">...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page-pager flex items-center justify-between px-4 py-2 border-t border-slate-200 bg-slate-50 text-xs text-slate-600">
|
||||||
|
<span>Showing 1-10 of 50</span>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="px-2 py-1 border border-slate-200 rounded text-xs">Prev</button>
|
||||||
|
<span class="text-[11px]">Page 1 / 5</span>
|
||||||
|
<button class="px-2 py-1 border border-slate-200 rounded text-xs">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Quy ước table:
|
||||||
|
|
||||||
|
- Header sticky.
|
||||||
|
- Row hover nhẹ.
|
||||||
|
- Cell padding đều.
|
||||||
|
- Text trong bảng ưu tiên `text-sm`.
|
||||||
|
- Header dùng `text-[10px] uppercase`.
|
||||||
|
- Nếu nhiều cột, đặt `min-width` và cho horizontal scroll.
|
||||||
|
- Nếu có action column quan trọng, có thể sticky bên phải.
|
||||||
|
|
||||||
|
Sticky action column:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<td class="sticky right-0 z-10 bg-white group-hover:bg-slate-50/80 px-4 py-3 text-right shadow-[-10px_0_12px_-12px_rgba(15,23,42,0.45)]">
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Filter/Search Pattern
|
||||||
|
|
||||||
|
Filter bar:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="page-filters flex items-center gap-3 mb-4">
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<span class="text-[10px] font-bold uppercase text-on-surface-variant">Status</span>
|
||||||
|
<select class="bg-surface-container-low border-slate-200 rounded-md text-[11px] py-1 px-2 pr-6 focus:ring-1 focus:ring-primary shadow-sm">
|
||||||
|
<option>All</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5 flex-1">
|
||||||
|
<span class="text-[10px] font-bold uppercase text-on-surface-variant">Search</span>
|
||||||
|
<input class="flex-1 bg-surface-container-low border-slate-200 rounded-md text-[11px] py-1 px-2 focus:ring-1 focus:ring-primary shadow-sm" placeholder="Search...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Nguyên tắc:
|
||||||
|
|
||||||
|
- Filter ngắn, đặt sát bảng.
|
||||||
|
- Search chiếm phần còn lại.
|
||||||
|
- Label filter nhỏ và uppercase.
|
||||||
|
- Trên mobile, filter chuyển thành column/full width.
|
||||||
|
|
||||||
|
## 11. Button Pattern
|
||||||
|
|
||||||
|
Primary:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button class="bg-primary hover:bg-primary-dim text-on-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95">
|
||||||
|
<span class="material-symbols-outlined text-base">add</span>
|
||||||
|
Add New
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Secondary:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button class="border border-slate-300 hover:bg-slate-100 text-slate-700 px-3 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95">
|
||||||
|
<span class="material-symbols-outlined text-base">download</span>
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Danger:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg text-xs font-bold">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Icon action:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button class="p-1.5 text-slate-400 transition-colors hover:text-primary" title="Edit">
|
||||||
|
<span class="material-symbols-outlined text-lg">edit</span>
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Disabled:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<button class="opacity-50 cursor-not-allowed" disabled>Disabled</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Form Pattern
|
||||||
|
|
||||||
|
Form field:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Name</label>
|
||||||
|
<input class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="Enter name">
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Readonly:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input readonly class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">
|
||||||
|
```
|
||||||
|
|
||||||
|
Textarea:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<textarea class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none"></textarea>
|
||||||
|
```
|
||||||
|
|
||||||
|
Field with icon:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="relative">
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
|
||||||
|
<span class="material-symbols-outlined text-base">person</span>
|
||||||
|
</span>
|
||||||
|
<input class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent text-sm font-medium">
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 13. Badge/Status Pattern
|
||||||
|
|
||||||
|
Online/offline:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="flex items-center gap-1.5">
|
||||||
|
<div class="w-1.5 h-1.5 rounded-full bg-primary ring-2 ring-primary/20"></div>
|
||||||
|
<span class="text-xs font-medium text-on-primary-fixed-variant">Online</span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Status badges:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="inline-block px-2 py-1 rounded text-xs font-semibold bg-emerald-100 text-emerald-700">Trong kho</span>
|
||||||
|
<span class="inline-block px-2 py-1 rounded text-xs font-semibold bg-blue-100 text-blue-700">Đang sử dụng</span>
|
||||||
|
<span class="inline-block px-2 py-1 rounded text-xs font-semibold bg-yellow-100 text-yellow-700">Đang chờ</span>
|
||||||
|
<span class="inline-block px-2 py-1 rounded text-xs font-semibold bg-red-100 text-red-700">Từ chối</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
Role chip:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-700">Admin</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 14. Modal Pattern
|
||||||
|
|
||||||
|
Backdrop + content:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="exampleModal">
|
||||||
|
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
|
||||||
|
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
|
||||||
|
<h3 class="text-base font-extrabold text-slate-900">Modal Title</h3>
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="p-6 space-y-4">
|
||||||
|
<!-- Fields -->
|
||||||
|
<div class="flex gap-3 pt-4">
|
||||||
|
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg">Cancel</button>
|
||||||
|
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Modal CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.modal-backdrop {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop.open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
transform: scale(0.95);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop.open .modal-content {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Danger confirm modal:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="px-6 py-4 border-b border-slate-100 bg-red-50 flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-red-600 text-2xl">warning</span>
|
||||||
|
<h3 class="text-base font-extrabold text-red-700">Xác nhận xóa</h3>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15. Login/Auth Screen Pattern
|
||||||
|
|
||||||
|
Auth screen dùng centered card:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<body class="bg-gradient-to-br from-blue-50 via-background to-purple-50 antialiased">
|
||||||
|
<div class="min-h-screen flex items-center justify-center px-4">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<div class="bg-white rounded-2xl shadow-lg border border-outline-variant/10 p-8">
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="flex justify-center mb-4">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined text-primary text-4xl">security</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-black text-slate-900 tracking-tight">Product Name</h1>
|
||||||
|
<p class="text-xs uppercase tracking-widest text-on-surface-variant font-bold mt-2">Admin Console</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
Auth tabs:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="flex gap-2 mb-6" role="tablist">
|
||||||
|
<button class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-primary text-on-primary shadow-sm">Đăng nhập</button>
|
||||||
|
<button class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-surface-container-low text-on-surface-variant">Đăng ký</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Alert states:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium">Error message</div>
|
||||||
|
<div class="bg-green-50 text-green-800 border border-green-200 rounded-lg px-4 py-3 text-xs font-medium">Success message</div>
|
||||||
|
<div class="bg-amber-50 text-amber-800 border border-amber-200 rounded-lg px-4 py-3 text-xs font-medium">Warning message</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 16. Responsive Rules
|
||||||
|
|
||||||
|
Recommended CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
#mobileMenuBtn,
|
||||||
|
#sidebarBackdrop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
body.app-shell {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100dvh;
|
||||||
|
height: 100dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobileMenuBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebarBackdrop {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 70;
|
||||||
|
background: rgba(15, 23, 42, 0.45);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#appSidebar {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
height: 100dvh;
|
||||||
|
width: min(82vw, 16rem);
|
||||||
|
z-index: 80;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-nav-open #sidebarBackdrop {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.mobile-nav-open #appSidebar {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header,
|
||||||
|
.page-filters,
|
||||||
|
.users-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap table {
|
||||||
|
min-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-pager {
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop .modal-content {
|
||||||
|
width: calc(100% - 1rem);
|
||||||
|
max-height: min(88dvh, 700px);
|
||||||
|
margin: 0.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.dashboard-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Mobile behavior:
|
||||||
|
|
||||||
|
- Sidebar thành off-canvas.
|
||||||
|
- Topbar gọn lại.
|
||||||
|
- Profile metadata nên ẩn.
|
||||||
|
- Header/filter chuyển column.
|
||||||
|
- Table luôn scroll ngang nếu nhiều cột.
|
||||||
|
- Modal gần bottom và giới hạn chiều cao.
|
||||||
|
|
||||||
|
## 17. UX Rules
|
||||||
|
|
||||||
|
Giữ những nguyên tắc này khi áp dụng sang dự án khác:
|
||||||
|
|
||||||
|
1. Mỗi màn dữ liệu cần có search hoặc filter nếu danh sách có thể dài.
|
||||||
|
2. Action chính luôn rõ ràng, nằm ở page header.
|
||||||
|
3. Action nguy hiểm luôn cần confirm.
|
||||||
|
4. Action theo quyền nên disabled trực quan, không chỉ báo lỗi sau khi bấm.
|
||||||
|
5. Table phải có empty state.
|
||||||
|
6. Table dài cần pagination.
|
||||||
|
7. Form dài nên chia grid 2 cột trên desktop, 1 cột mobile.
|
||||||
|
8. Modal dài phải scroll trong content, không làm tràn viewport.
|
||||||
|
9. Badge trạng thái phải thống nhất màu theo ý nghĩa.
|
||||||
|
10. Icon-only button phải có `title` hoặc accessible name.
|
||||||
|
11. Không dùng nhiều card trang trí trong dashboard vận hành.
|
||||||
|
12. Text không được overlap hoặc tràn button/card.
|
||||||
|
|
||||||
|
## 18. Accessibility Checklist
|
||||||
|
|
||||||
|
Khi port sang dự án mới, nên bổ sung:
|
||||||
|
|
||||||
|
- `aria-label` cho icon-only buttons.
|
||||||
|
- `role="dialog"` và `aria-modal="true"` cho modal.
|
||||||
|
- Focus trap trong modal.
|
||||||
|
- Đóng modal bằng phím `Escape`.
|
||||||
|
- `aria-expanded` cho mobile menu.
|
||||||
|
- `aria-selected` cho tabs.
|
||||||
|
- Label đầy đủ cho form input.
|
||||||
|
- Không chỉ dùng màu để truyền đạt trạng thái; nên có text status.
|
||||||
|
- Contrast đủ tốt cho text nhỏ.
|
||||||
|
|
||||||
|
## 19. Implementation Checklist
|
||||||
|
|
||||||
|
Khi bắt đầu dự án mới:
|
||||||
|
|
||||||
|
1. Cài Tailwind và plugin forms nếu cần.
|
||||||
|
2. Copy design tokens vào `tailwind.config.js`.
|
||||||
|
3. Thêm Google Fonts và Material Symbols.
|
||||||
|
4. Tạo app shell: sidebar, topbar, content area.
|
||||||
|
5. Tạo component base cho button, table, filter, modal, badge.
|
||||||
|
6. Tạo responsive sidebar theo breakpoint `900px`.
|
||||||
|
7. Áp dụng page layout pattern cho từng màn hình.
|
||||||
|
8. Kiểm tra table trên mobile với horizontal scroll.
|
||||||
|
9. Chuẩn hóa status color map.
|
||||||
|
10. Thêm empty/loading/error states.
|
||||||
|
|
||||||
|
## 20. Mapping Sang Dự Án Khác
|
||||||
|
|
||||||
|
Nếu dự án mới không phải quản lý tài khoản/tài sản, vẫn có thể dùng style này bằng cách đổi tên module:
|
||||||
|
|
||||||
|
| AccManager concept | Dự án mới có thể thay bằng |
|
||||||
|
| --- | --- |
|
||||||
|
| Applications | Products, Services, Modules, Integrations |
|
||||||
|
| Accounts | Records, Credentials, Customers, Contracts |
|
||||||
|
| Assets | Inventory, Devices, Documents, Items |
|
||||||
|
| Users | Members, Staff, Operators |
|
||||||
|
| Departments | Teams, Groups, Categories |
|
||||||
|
| Projects | Campaigns, Workspaces, Sites |
|
||||||
|
| Borrow/Return | Requests, Tickets, Approvals, Workflows |
|
||||||
|
|
||||||
|
Điều quan trọng là giữ cùng cấu trúc:
|
||||||
|
|
||||||
|
- Sidebar nhóm chức năng.
|
||||||
|
- Page header + actions.
|
||||||
|
- Filter/search.
|
||||||
|
- Table/list.
|
||||||
|
- Modal CRUD.
|
||||||
|
- Badge trạng thái.
|
||||||
|
- Toast feedback.
|
||||||
|
|
||||||
|
## 21. Những Gì Nên Tránh Khi Port
|
||||||
|
|
||||||
|
- Copy nguyên text nghiệp vụ AccManager nếu dự án mới khác domain.
|
||||||
|
- Copy table quá nhiều cột khi domain không cần.
|
||||||
|
- Lạm dụng modal cho mọi thứ nếu workflow mới cần full page editor.
|
||||||
|
- Dùng gradient nền cho app shell chính.
|
||||||
|
- Tăng border radius quá lớn làm mất chất admin console.
|
||||||
|
- Dùng font khác quá mềm hoặc decorative.
|
||||||
|
- Thêm quá nhiều shadow, glassmorphism, decorative blobs.
|
||||||
|
- Trộn nhiều style icon khác nhau.
|
||||||
|
|
||||||
1143
backend/server.js
1143
backend/server.js
File diff suppressed because it is too large
Load Diff
@@ -168,7 +168,214 @@ SET NewQuantity = CASE
|
|||||||
END;
|
END;
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 5. CREATE ASSET DEPARTMENTS TABLE
|
-- 5. CREATE CONSUMABLE INVENTORY TABLE
|
||||||
|
-- ===========================================
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ConsumableInventory')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE ConsumableInventory (
|
||||||
|
ConsumableId INT PRIMARY KEY IDENTITY(1,1),
|
||||||
|
RequestMonth NVARCHAR(50) NULL,
|
||||||
|
ConsumableCode NVARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
ConsumableName NVARCHAR(255) NOT NULL,
|
||||||
|
Model NVARCHAR(255) NULL,
|
||||||
|
Unit NVARCHAR(50) NULL,
|
||||||
|
OpeningBalance INT NOT NULL DEFAULT 0,
|
||||||
|
ImportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
|
ExportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
|
EndingBalance INT NOT NULL DEFAULT 0,
|
||||||
|
ExportReason NVARCHAR(1000) NULL,
|
||||||
|
CreatedBy INT NULL,
|
||||||
|
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
|
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
|
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
PRINT 'Table ConsumableInventory created successfully.';
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableInventory', 'RequestMonth') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableInventory ADD RequestMonth NVARCHAR(50) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableInventory', 'Model') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableInventory ADD Model NVARCHAR(255) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableInventory', 'Unit') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableInventory ADD Unit NVARCHAR(50) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableInventory', 'OpeningBalance') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableInventory ADD OpeningBalance INT NOT NULL CONSTRAINT DF_ConsumableInventory_OpeningBalance DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableInventory', 'ImportInPeriod') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableInventory ADD ImportInPeriod INT NOT NULL CONSTRAINT DF_ConsumableInventory_ImportInPeriod DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableInventory', 'ExportInPeriod') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableInventory ADD ExportInPeriod INT NOT NULL CONSTRAINT DF_ConsumableInventory_ExportInPeriod DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableInventory', 'EndingBalance') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableInventory ADD EndingBalance INT NOT NULL CONSTRAINT DF_ConsumableInventory_EndingBalance DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableInventory', 'ExportReason') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableInventory ADD ExportReason NVARCHAR(1000) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
-- ===========================================
|
||||||
|
-- 6. CREATE CONSUMABLE EXPORT HISTORY TABLE
|
||||||
|
-- ===========================================
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ConsumableExportHistory')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE ConsumableExportHistory (
|
||||||
|
ExportHistoryId INT PRIMARY KEY IDENTITY(1,1),
|
||||||
|
ConsumableId INT NOT NULL,
|
||||||
|
ConsumableCode NVARCHAR(100) NOT NULL,
|
||||||
|
ConsumableName NVARCHAR(255) NOT NULL,
|
||||||
|
Unit NVARCHAR(50) NULL,
|
||||||
|
ExportQuantity INT NOT NULL DEFAULT 1,
|
||||||
|
RecipientName NVARCHAR(100) NOT NULL,
|
||||||
|
ProjectName NVARCHAR(150) NULL,
|
||||||
|
ExportedByName NVARCHAR(100) NOT NULL,
|
||||||
|
ExportNote NVARCHAR(1000) NULL,
|
||||||
|
PreviousExportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
|
NextExportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
|
PreviousEndingBalance INT NOT NULL DEFAULT 0,
|
||||||
|
NextEndingBalance INT NOT NULL DEFAULT 0,
|
||||||
|
CreatedBy INT NULL,
|
||||||
|
ExportedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
|
CreatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
|
UpdatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
|
FOREIGN KEY (ConsumableId) REFERENCES ConsumableInventory(ConsumableId) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
PRINT 'Table ConsumableExportHistory created successfully.';
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'ConsumableCode') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD ConsumableCode NVARCHAR(100) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'ConsumableName') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD ConsumableName NVARCHAR(255) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'Unit') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD Unit NVARCHAR(50) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'ExportQuantity') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD ExportQuantity INT NOT NULL CONSTRAINT DF_ConsumableExportHistory_ExportQuantity DEFAULT(1);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'RecipientName') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD RecipientName NVARCHAR(100) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'ProjectName') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD ProjectName NVARCHAR(150) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'ExportedByName') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD ExportedByName NVARCHAR(100) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'ExportNote') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD ExportNote NVARCHAR(1000) NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'PreviousExportInPeriod') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD PreviousExportInPeriod INT NOT NULL CONSTRAINT DF_ConsumableExportHistory_PreviousExportInPeriod DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'NextExportInPeriod') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD NextExportInPeriod INT NOT NULL CONSTRAINT DF_ConsumableExportHistory_NextExportInPeriod DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'PreviousEndingBalance') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD PreviousEndingBalance INT NOT NULL CONSTRAINT DF_ConsumableExportHistory_PreviousEndingBalance DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'NextEndingBalance') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD NextEndingBalance INT NOT NULL CONSTRAINT DF_ConsumableExportHistory_NextEndingBalance DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'CreatedBy') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD CreatedBy INT NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'ExportedDate') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_ConsumableExportHistory_ExportedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'CreatedDate') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_ConsumableExportHistory_CreatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.ConsumableExportHistory', 'UpdatedDate') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_ConsumableExportHistory_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys.foreign_key_columns fkc
|
||||||
|
INNER JOIN sys.columns c
|
||||||
|
ON c.object_id = fkc.parent_object_id
|
||||||
|
AND c.column_id = fkc.parent_column_id
|
||||||
|
WHERE fkc.parent_object_id = OBJECT_ID('dbo.ConsumableExportHistory')
|
||||||
|
AND c.name = 'ConsumableId'
|
||||||
|
)
|
||||||
|
AND COL_LENGTH('dbo.ConsumableExportHistory', 'ConsumableId') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory
|
||||||
|
ADD CONSTRAINT FK_ConsumableExportHistory_ConsumableId
|
||||||
|
FOREIGN KEY (ConsumableId) REFERENCES ConsumableInventory(ConsumableId) ON DELETE CASCADE;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys.foreign_key_columns fkc
|
||||||
|
INNER JOIN sys.columns c
|
||||||
|
ON c.object_id = fkc.parent_object_id
|
||||||
|
AND c.column_id = fkc.parent_column_id
|
||||||
|
WHERE fkc.parent_object_id = OBJECT_ID('dbo.ConsumableExportHistory')
|
||||||
|
AND c.name = 'CreatedBy'
|
||||||
|
)
|
||||||
|
AND COL_LENGTH('dbo.ConsumableExportHistory', 'CreatedBy') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE ConsumableExportHistory
|
||||||
|
ADD CONSTRAINT FK_ConsumableExportHistory_CreatedBy
|
||||||
|
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL;
|
||||||
|
END
|
||||||
|
|
||||||
|
-- ===========================================
|
||||||
|
-- 7. CREATE ASSET DEPARTMENTS TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDepartments')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDepartments')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -197,7 +404,7 @@ WHERE NOT EXISTS (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 6. CREATE ASSET PROJECTS TABLE
|
-- 8. CREATE ASSET PROJECTS TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetProjects')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetProjects')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -211,7 +418,7 @@ BEGIN
|
|||||||
END
|
END
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 7. CREATE ASSET BORROW REQUESTS TABLE
|
-- 9. CREATE ASSET BORROW REQUESTS TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequests')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequests')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -386,7 +593,7 @@ BEGIN
|
|||||||
END
|
END
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 8. CREATE ASSET EXPORT HISTORY TABLE
|
-- 10. CREATE ASSET EXPORT HISTORY TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetExportHistory')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetExportHistory')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -484,7 +691,7 @@ BEGIN
|
|||||||
END
|
END
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 9. CREATE ASSET DAMAGE/DISPOSAL HISTORY TABLE
|
-- 11. CREATE ASSET DAMAGE/DISPOSAL HISTORY TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDamageDisposalHistory')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDamageDisposalHistory')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -522,7 +729,7 @@ BEGIN
|
|||||||
END
|
END
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 10. CREATE AUDIT LOG TABLE
|
-- 12. CREATE AUDIT LOG TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -541,7 +748,7 @@ BEGIN
|
|||||||
END
|
END
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 11. CREATE INDEXES
|
-- 13. CREATE INDEXES
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -573,6 +780,31 @@ BEGIN
|
|||||||
CREATE INDEX IX_AssetInventory_Department ON AssetInventory(Department);
|
CREATE INDEX IX_AssetInventory_Department ON AssetInventory(Department);
|
||||||
END
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ConsumableInventory_ConsumableCode')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_ConsumableInventory_ConsumableCode ON ConsumableInventory(ConsumableCode);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ConsumableInventory_RequestMonth')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_ConsumableInventory_RequestMonth ON ConsumableInventory(RequestMonth);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ConsumableInventory_EndingBalance')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_ConsumableInventory_EndingBalance ON ConsumableInventory(EndingBalance);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ConsumableExportHistory_ConsumableId')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_ConsumableExportHistory_ConsumableId ON ConsumableExportHistory(ConsumableId);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ConsumableExportHistory_ExportedDate')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_ConsumableExportHistory_ExportedDate ON ConsumableExportHistory(ExportedDate DESC);
|
||||||
|
END
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UX_AssetDepartments_DepartmentName')
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UX_AssetDepartments_DepartmentName')
|
||||||
BEGIN
|
BEGIN
|
||||||
CREATE UNIQUE INDEX UX_AssetDepartments_DepartmentName ON AssetDepartments(DepartmentName);
|
CREATE UNIQUE INDEX UX_AssetDepartments_DepartmentName ON AssetDepartments(DepartmentName);
|
||||||
|
|||||||
1501
public/js/app.js
1501
public/js/app.js
File diff suppressed because it is too large
Load Diff
@@ -300,6 +300,198 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add/Edit Consumable Modal -->
|
||||||
|
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="consumableModal">
|
||||||
|
<div class="modal-content bg-white rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
|
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
|
||||||
|
<h3 class="text-base font-extrabold text-slate-900" id="consumableModalTitle">Thêm vật tư tiêu hao</h3>
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeConsumableModal()">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="consumableForm" class="p-6 space-y-4 overflow-y-auto">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tháng đề xuất</label>
|
||||||
|
<select id="consumableRequestMonthInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
|
||||||
|
<option value="">-- Chọn tháng --</option>
|
||||||
|
<option value="Tháng 1">Tháng 1</option>
|
||||||
|
<option value="Tháng 2">Tháng 2</option>
|
||||||
|
<option value="Tháng 3">Tháng 3</option>
|
||||||
|
<option value="Tháng 4">Tháng 4</option>
|
||||||
|
<option value="Tháng 5">Tháng 5</option>
|
||||||
|
<option value="Tháng 6">Tháng 6</option>
|
||||||
|
<option value="Tháng 7">Tháng 7</option>
|
||||||
|
<option value="Tháng 8">Tháng 8</option>
|
||||||
|
<option value="Tháng 9">Tháng 9</option>
|
||||||
|
<option value="Tháng 10">Tháng 10</option>
|
||||||
|
<option value="Tháng 11">Tháng 11</option>
|
||||||
|
<option value="Tháng 12">Tháng 12</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Mã vật tư</label>
|
||||||
|
<input type="text" id="consumableCodeInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="Để trống để hệ thống tự tạo">
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên linh kiện/sp</label>
|
||||||
|
<input type="text" id="consumableNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Hub type C">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Model</label>
|
||||||
|
<input type="text" id="consumableModelInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="Hub Type C">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">ĐVT</label>
|
||||||
|
<input type="text" id="consumableUnitInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="Cái">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tồn đầu kỳ</label>
|
||||||
|
<input type="number" id="consumableOpeningBalanceInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="0">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Nhập trong kì</label>
|
||||||
|
<input type="number" id="consumableImportInPeriodInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="0">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Xuất trong kì</label>
|
||||||
|
<input type="number" id="consumableExportInPeriodInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="0">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tồn cuối kì</label>
|
||||||
|
<input type="number" id="consumableEndingBalanceInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" value="0" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Lý do xuất</label>
|
||||||
|
<textarea id="consumableExportReasonInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Xuất cho bộ phận/dự án/người nhận..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 pt-2">
|
||||||
|
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeConsumableModal()">Hủy</button>
|
||||||
|
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Lưu vật tư</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Consumable Modal -->
|
||||||
|
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteConsumableModal">
|
||||||
|
<div class="modal-content bg-white rounded-2xl shadow-2xl w-full max-w-md p-6">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-red-100 text-red-600 flex items-center justify-center">
|
||||||
|
<span class="material-symbols-outlined">delete</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-extrabold text-slate-900">Xóa vật tư tiêu hao</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa vật tư <strong id="deleteConsumableName">-</strong>? Hành động này không thể hoàn tác.</p>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeDeleteConsumableModal()">Hủy</button>
|
||||||
|
<button type="button" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold confirm-delete-consumable">Xóa</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export Consumable Modal -->
|
||||||
|
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="consumableExportModal">
|
||||||
|
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
|
||||||
|
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
|
||||||
|
<h3 class="text-base font-extrabold text-slate-900">Xuất vật tư tiêu hao</h3>
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeConsumableExportModal()">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="consumableExportForm" class="p-6 space-y-4">
|
||||||
|
<input type="hidden" id="consumableExportConsumableIdInput">
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Vật tư</label>
|
||||||
|
<input type="text" id="consumableExportConsumableNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tồn cuối kỳ hiện tại</label>
|
||||||
|
<input type="number" id="consumableExportCurrentEndingInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Xuất cho</label>
|
||||||
|
<select id="consumableExportTargetTypeInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
|
||||||
|
<option value="user">Người dùng</option>
|
||||||
|
<option value="project">Dự án</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng xuất</label>
|
||||||
|
<input type="number" id="consumableExportQuantityInput" min="1" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="1" required>
|
||||||
|
</div>
|
||||||
|
<div id="consumableExportProjectField" class="hidden">
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Dự án nhận</label>
|
||||||
|
<select id="consumableExportProjectInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
|
||||||
|
<option value="">-- Chọn dự án --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label id="consumableExportRecipientLabel" class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người nhận</label>
|
||||||
|
<select id="consumableExportUserInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
|
||||||
|
<option value="">-- Chọn người nhận --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người xuất</label>
|
||||||
|
<input type="text" id="consumableExportActorInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Role người thao tác</label>
|
||||||
|
<input type="text" id="consumableExportRoleInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ghi chú xuất</label>
|
||||||
|
<textarea id="consumableExportNoteInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Nhập mục đích sử dụng hoặc ghi chú bàn giao"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 pt-2">
|
||||||
|
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeConsumableExportModal()">Hủy</button>
|
||||||
|
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Xác nhận xuất</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Consumable Export History Modal -->
|
||||||
|
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="consumableExportHistoryModal" style="z-index: 124;">
|
||||||
|
<div class="modal-content w-full max-w-6xl bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4 flex flex-col" style="max-height: calc(100vh - 2rem);">
|
||||||
|
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
|
||||||
|
<h3 class="text-base font-extrabold text-slate-900">Lịch sử xuất vật tư tiêu hao</h3>
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeConsumableExportHistoryModal()">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 pt-4 overflow-auto">
|
||||||
|
<div class="rounded-xl border border-slate-200 overflow-hidden">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-left border-collapse" style="min-width: 1100px;">
|
||||||
|
<thead class="bg-slate-50 border-b border-slate-200">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày giờ</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Vật tư</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Số lượng</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người nhận</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Dự án nhận</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người xuất</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Tồn trước/sau</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="consumableExportHistoryTableBody" class="divide-y divide-slate-100">
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có dữ liệu lịch sử xuất vật tư.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Export Asset Modal -->
|
<!-- Export Asset Modal -->
|
||||||
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="borrowAssetModal">
|
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="borrowAssetModal">
|
||||||
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
|
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
|
||||||
|
|||||||
@@ -259,6 +259,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="tree-label">Vật tư tiêu hao</div>
|
||||||
|
<div class="tree-branch">
|
||||||
|
<a href="#consumables" data-nav="consumables" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
|
||||||
|
<span class="material-symbols-outlined">category</span>
|
||||||
|
<span>Danh sách VTTH</span>
|
||||||
|
</a>
|
||||||
|
<a href="#consumable-exports" data-nav="consumable-exports" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
|
||||||
|
<span class="material-symbols-outlined">outbox</span>
|
||||||
|
<span>Lịch sử xuất</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="usersSection" class="pt-2 border-t border-outline-variant/10" style="display: none;">
|
<div id="usersSection" class="pt-2 border-t border-outline-variant/10" style="display: none;">
|
||||||
<a id="usersNav" href="#users" data-nav="users" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-lg" style="display: none;">
|
<a id="usersNav" href="#users" data-nav="users" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-lg" style="display: none;">
|
||||||
<span class="material-symbols-outlined">people</span>
|
<span class="material-symbols-outlined">people</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user