From 5fa11b588aa9f978ddff1b7de9f7cdc2e6af9a30 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Sun, 12 Jul 2026 19:03:37 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20Supplier:=20import=20v2=20=E2=80=94?= =?UTF-8?q?=20Mig=2064=20publish/draft=20+=20dedup-MST=20+=20template=20+?= =?UTF-8?q?=20file=20m=E1=BA=ABu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mig 64 AddSupplierPublishState: +IsPublic (draft/public); backfill 22 prod->public; filtered-unique doi [Code]<>'' (cho phep nhieu nhap Code=''); no new table (89). - Import v2: dedup MST-primary + Code-backstop (chong 500 unique-violation); re-bake 30 token header THAT byte-exact (LayoutValid khop file that); nhap Ma NCC per-row (=Code); thieu Ma NCC->nhap (IsPublic=false); MST thieu->canh bao mem (MstMissing). - PublishSupplierCommand rieng (ne #73 clobber); GET /suppliers/import/template (BE-gen xlsx 30-col). - FE 2-app SHA-mirror: badge Public/Nhap; nut Cong bo; filter; nut Tai file mau; cot Ma editable; canh bao MST. Picker PE + tao HD loc published=true (an nhap -> ma HD khong dinh Code rong). CreateSupplier set IsPublic=true. - authz D3: import/preview/confirm/template/publish = Policy Suppliers.Update (khop FE PermissionGuard, het 403). - Tests +19 (477 PASS): dedup T1-T7, publish-guard, list-filter, all-or-nothing, LayoutValid. Fix null-Code NRE (path R4 loi). Co-Authored-By: Claude Opus 4.8 --- .../master/SupplierImportDialog.tsx | 60 +- fe-admin/src/components/pe/PeDetailTabs.tsx | 2 +- .../pages/contracts/ContractCreatePage.tsx | 2 +- fe-admin/src/pages/master/SuppliersPage.tsx | 121 +- fe-admin/src/types/master.ts | 2 + .../master/SupplierImportDialog.tsx | 60 +- fe-user/src/components/pe/PeDetailTabs.tsx | 2 +- .../pages/contracts/ContractCreatePage.tsx | 2 +- fe-user/src/pages/master/SuppliersPage.tsx | 121 +- fe-user/src/types/master.ts | 2 + .../Controllers/SuppliersController.cs | 34 +- .../CreateSupplier/CreateSupplierCommand.cs | 1 + .../PublishSupplier/PublishSupplierCommand.cs | 57 + .../Master/Suppliers/Dtos/SupplierDto.cs | 4 +- .../Suppliers/Dtos/SupplierImportDtos.cs | 8 +- .../Import/ISupplierExcelImportService.cs | 13 +- .../Queries/GetSupplier/GetSupplierQuery.cs | 2 +- .../GetSupplierImportTemplateQuery.cs | 17 + .../ListSuppliers/ListSuppliersQuery.cs | 11 +- .../SolutionErp.Domain/Master/Supplier.cs | 5 + .../Configurations/SupplierConfiguration.cs | 8 +- .../Persistence/DbInitializer.cs | 3 + ...110556_AddSupplierPublishState.Designer.cs | 6360 +++++++++++++++++ .../20260712110556_AddSupplierPublishState.cs | 55 + .../ApplicationDbContextModelSnapshot.cs | 7 +- .../Services/SupplierExcelImportService.cs | 198 +- .../SupplierPublishAndListTests.cs | 155 + .../Services/SupplierImportV2DedupTests.cs | 452 ++ 28 files changed, 7659 insertions(+), 105 deletions(-) create mode 100644 src/Backend/SolutionErp.Application/Master/Suppliers/Commands/PublishSupplier/PublishSupplierCommand.cs create mode 100644 src/Backend/SolutionErp.Application/Master/Suppliers/Queries/GetSupplierImportTemplate/GetSupplierImportTemplateQuery.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260712110556_AddSupplierPublishState.Designer.cs create mode 100644 src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260712110556_AddSupplierPublishState.cs create mode 100644 tests/SolutionErp.Infrastructure.Tests/Application/SupplierPublishAndListTests.cs create mode 100644 tests/SolutionErp.Infrastructure.Tests/Services/SupplierImportV2DedupTests.cs diff --git a/fe-admin/src/components/master/SupplierImportDialog.tsx b/fe-admin/src/components/master/SupplierImportDialog.tsx index ba7c096..51937eb 100644 --- a/fe-admin/src/components/master/SupplierImportDialog.tsx +++ b/fe-admin/src/components/master/SupplierImportDialog.tsx @@ -1,9 +1,10 @@ import { useEffect, useRef, useState, type ChangeEvent, type DragEvent } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { AlertTriangle, FileSpreadsheet, Loader2, Upload } from 'lucide-react' +import { AlertTriangle, Download, FileSpreadsheet, Loader2, Upload } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/Button' import { Dialog } from '@/components/ui/Dialog' +import { Input } from '@/components/ui/Input' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' @@ -58,6 +59,8 @@ export type SupplierImportRow = { status: RowImportStatus messages: string[] existingSupplierId: string | null + // Thiếu MST → cảnh báo mềm (không kiểm được trùng), KHÔNG chặn nhập (S113) + mstMissing: boolean } export type SupplierImportPreview = { @@ -145,6 +148,27 @@ export function SupplierImportDialog({ open, onClose }: { open: boolean; onClose onError: err => toast.error(getErrorMessage(err)), }) + // Tải file mẫu .xlsx trống (BE dựng bằng ClosedXML — single-source layout, 0 drift). + const templateMut = useMutation({ + mutationFn: async () => { + const res = await api.get('/suppliers/import/template', { responseType: 'blob' }) + const url = window.URL.createObjectURL(res.data as Blob) + const a = document.createElement('a') + a.href = url + a.download = 'Mau-Database-NCC.xlsx' + a.click() + window.URL.revokeObjectURL(url) + }, + onError: err => toast.error(getErrorMessage(err)), + }) + + // Sửa Mã NCC per-dòng ngay trên lưới preview (round-trip verbatim sang confirm). + function updateRow(rowIndex: number, patch: Partial) { + setPreview(p => + p ? { ...p, rows: p.rows.map(x => (x.rowIndex === rowIndex ? { ...x, ...patch } : x)) } : p, + ) + } + function pickFile(files: FileList | null) { const file = files?.[0] if (!file) return @@ -228,6 +252,21 @@ export function SupplierImportDialog({ open, onClose }: { open: boolean; onClose + {/* Tải file mẫu — lấy layout chuẩn trước khi nhập */} +
+

Chưa có file? Tải mẫu chuẩn rồi điền dữ liệu NCC theo đúng cột.

+ +
+ {/* Đang phân tích */} {previewMut.isPending && (
@@ -281,6 +320,11 @@ export function SupplierImportDialog({ open, onClose }: { open: boolean; onClose
)} +

+ Nhập Mã NCC cho từng dòng ở cột "Mã". NCC chưa nhập Mã NCC sẽ lưu ở trạng thái{' '} + Nháp (ẩn), chưa hiển thị ra ngoài. +

+
@@ -300,16 +344,24 @@ export function SupplierImportDialog({ open, onClose }: { open: boolean; onClose - + ))} diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index b06512a..2dca7ce 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -2219,7 +2219,7 @@ function AddSupplierDialog({ evaluationId, detailId, onClose }: { const qc = useQueryClient() const suppliers = useQuery({ queryKey: ['all-suppliers'], - queryFn: async () => (await api.get<{ items: Supplier[] }>('/suppliers', { params: { pageSize: 1000 } })).data.items, + queryFn: async () => (await api.get<{ items: Supplier[] }>('/suppliers', { params: { pageSize: 1000, published: true } })).data.items, // S113 R4: chỉ NCC đã công bố (ẩn nháp) }) const [form, setForm] = useState({ supplierId: '', diff --git a/fe-admin/src/pages/contracts/ContractCreatePage.tsx b/fe-admin/src/pages/contracts/ContractCreatePage.tsx index 99553a8..5ec9061 100644 --- a/fe-admin/src/pages/contracts/ContractCreatePage.tsx +++ b/fe-admin/src/pages/contracts/ContractCreatePage.tsx @@ -318,7 +318,7 @@ function ContractHeaderForm({ const suppliers = useQuery({ queryKey: ['suppliers-all'], - queryFn: async () => (await api.get>('/suppliers', { params: { page: 1, pageSize: 200 } })).data.items, + queryFn: async () => (await api.get>('/suppliers', { params: { page: 1, pageSize: 200, published: true } })).data.items, // S113 R4: chỉ NCC đã công bố (ẩn nháp → mã HĐ không dính Code rỗng) }) const projects = useQuery({ queryKey: ['projects-all'], diff --git a/fe-admin/src/pages/master/SuppliersPage.tsx b/fe-admin/src/pages/master/SuppliersPage.tsx index 0126872..6e26f8c 100644 --- a/fe-admin/src/pages/master/SuppliersPage.tsx +++ b/fe-admin/src/pages/master/SuppliersPage.tsx @@ -1,6 +1,6 @@ import { useState, type FormEvent } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { Pencil, Plus, Trash2, Upload } from 'lucide-react' +import { EyeOff, Globe, Pencil, Plus, Trash2, Upload } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' import { DataTable, Pagination, type Column } from '@/components/DataTable' @@ -77,11 +77,24 @@ export function SuppliersPage() { const [form, setForm] = useState(emptyForm) const isEdit = !!form.id + // Bộ lọc công bố: '' = tất cả · 'true' = đã công bố · 'false' = nháp (ẩn) + const [published, setPublished] = useState('') + // Dialog công bố: nhập Mã NCC trước khi NCC hiển thị ra ngoài + const [publishTarget, setPublishTarget] = useState(null) + const [maNcc, setMaNcc] = useState('') + const list = useQuery({ - queryKey: ['suppliers', { page, search, sortBy, sortDesc }], + queryKey: ['suppliers', { page, search, sortBy, sortDesc, published }], queryFn: async () => { const res = await api.get>('/suppliers', { - params: { page, pageSize: 20, search: search || undefined, sortBy, sortDesc }, + params: { + page, + pageSize: 20, + search: search || undefined, + sortBy, + sortDesc, + published: published === '' ? undefined : published === 'true', + }, }) return res.data }, @@ -143,6 +156,24 @@ export function SuppliersPage() { onError: err => toast.error(getErrorMessage(err)), }) + // Công bố / ẩn NCC — KHÔNG qua UpdateSupplier (chỉ đổi Mã NCC + IsPublic). + const publish = useMutation({ + mutationFn: async ({ id, maNcc, doPublish }: { id: string; maNcc?: string; doPublish: boolean }) => + await api.post(`/suppliers/${id}/publish`, { maNcc: maNcc || undefined, publish: doPublish }), + onSuccess: (_data, vars) => { + qc.invalidateQueries({ queryKey: ['suppliers'] }) + toast.success(vars.doPublish ? 'Đã công bố NCC' : 'Đã ẩn NCC') + setPublishTarget(null) + setMaNcc('') + }, + onError: err => toast.error(getErrorMessage(err)), + }) + + function openPublish(s: Supplier) { + setPublishTarget(s) + setMaNcc(s.code ?? '') + } + function openNew() { setForm(emptyForm) setOpen(true) @@ -177,13 +208,47 @@ export function SuppliersPage() { { key: 'type', header: 'Loại', sortable: true, width: 'w-36', render: s => SupplierTypeLabel[s.type] }, { key: 'taxCode', header: 'MST', render: s => s.taxCode ?? '—' }, { key: 'phone', header: 'Điện thoại', render: s => s.phone ?? '—' }, + { + key: 'isPublic', + header: 'Trạng thái', + width: 'w-32', + render: s => + s.isPublic ? ( + + Đã công bố + + ) : ( + + Nháp (ẩn) + + ), + }, { key: 'actions', header: '', align: 'right', - width: 'w-32', + width: 'w-40', render: s => (
+ + {s.isPublic ? ( + + ) : ( + + )} +
+ setPublishTarget(null)} + title="Công bố nhà cung cấp" + size="sm" + footer={ + <> + + + + } + > +
+

+ Nhập Mã NCC để công bố "{publishTarget?.name}". Sau khi công bố, NCC sẽ hiển thị ở các màn chọn + NCC (duyệt NCC, hợp đồng). +

+
+ + setMaNcc(e.target.value)} placeholder="VD: NCC001" autoFocus /> +
+

NCC chưa có Mã NCC sẽ ở trạng thái Nháp (ẩn), chưa hiển thị ra ngoài.

+
+
+ setImportOpen(false)} /> ) diff --git a/fe-admin/src/types/master.ts b/fe-admin/src/types/master.ts index d43cf9a..4243b54 100644 --- a/fe-admin/src/types/master.ts +++ b/fe-admin/src/types/master.ts @@ -75,6 +75,8 @@ export type Supplier = { ownerPmh: string | null // Tình trạng status: SupplierStatus | null + // Công bố: true = hiển thị ra ngoài (picker NCC), false = nháp/ẩn (S113) + isPublic: boolean createdAt: string updatedAt: string | null } diff --git a/fe-user/src/components/master/SupplierImportDialog.tsx b/fe-user/src/components/master/SupplierImportDialog.tsx index ba7c096..51937eb 100644 --- a/fe-user/src/components/master/SupplierImportDialog.tsx +++ b/fe-user/src/components/master/SupplierImportDialog.tsx @@ -1,9 +1,10 @@ import { useEffect, useRef, useState, type ChangeEvent, type DragEvent } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { AlertTriangle, FileSpreadsheet, Loader2, Upload } from 'lucide-react' +import { AlertTriangle, Download, FileSpreadsheet, Loader2, Upload } from 'lucide-react' import { toast } from 'sonner' import { Button } from '@/components/ui/Button' import { Dialog } from '@/components/ui/Dialog' +import { Input } from '@/components/ui/Input' import { api } from '@/lib/api' import { getErrorMessage } from '@/lib/apiError' import { cn } from '@/lib/cn' @@ -58,6 +59,8 @@ export type SupplierImportRow = { status: RowImportStatus messages: string[] existingSupplierId: string | null + // Thiếu MST → cảnh báo mềm (không kiểm được trùng), KHÔNG chặn nhập (S113) + mstMissing: boolean } export type SupplierImportPreview = { @@ -145,6 +148,27 @@ export function SupplierImportDialog({ open, onClose }: { open: boolean; onClose onError: err => toast.error(getErrorMessage(err)), }) + // Tải file mẫu .xlsx trống (BE dựng bằng ClosedXML — single-source layout, 0 drift). + const templateMut = useMutation({ + mutationFn: async () => { + const res = await api.get('/suppliers/import/template', { responseType: 'blob' }) + const url = window.URL.createObjectURL(res.data as Blob) + const a = document.createElement('a') + a.href = url + a.download = 'Mau-Database-NCC.xlsx' + a.click() + window.URL.revokeObjectURL(url) + }, + onError: err => toast.error(getErrorMessage(err)), + }) + + // Sửa Mã NCC per-dòng ngay trên lưới preview (round-trip verbatim sang confirm). + function updateRow(rowIndex: number, patch: Partial) { + setPreview(p => + p ? { ...p, rows: p.rows.map(x => (x.rowIndex === rowIndex ? { ...x, ...patch } : x)) } : p, + ) + } + function pickFile(files: FileList | null) { const file = files?.[0] if (!file) return @@ -228,6 +252,21 @@ export function SupplierImportDialog({ open, onClose }: { open: boolean; onClose + {/* Tải file mẫu — lấy layout chuẩn trước khi nhập */} +
+

Chưa có file? Tải mẫu chuẩn rồi điền dữ liệu NCC theo đúng cột.

+ +
+ {/* Đang phân tích */} {previewMut.isPending && (
@@ -281,6 +320,11 @@ export function SupplierImportDialog({ open, onClose }: { open: boolean; onClose
)} +

+ Nhập Mã NCC cho từng dòng ở cột "Mã". NCC chưa nhập Mã NCC sẽ lưu ở trạng thái{' '} + Nháp (ẩn), chưa hiển thị ra ngoài. +

+
{r.code || '—'} + updateRow(r.rowIndex, { code: e.target.value })} + placeholder="Mã NCC" + className="h-7 font-mono text-xs" + /> + {r.name || '—'} {SupplierTypeLabel[r.type] ?? '—'} - {r.messages.length > 0 ? r.messages.join('; ') : ''} + {r.mstMissing &&
⚠️ Chưa có MST, có thể bị trùng
} + {r.messages.length > 0 &&
{r.messages.join('; ')}
}
@@ -300,16 +344,24 @@ export function SupplierImportDialog({ open, onClose }: { open: boolean; onClose - + ))} diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index b06512a..2dca7ce 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -2219,7 +2219,7 @@ function AddSupplierDialog({ evaluationId, detailId, onClose }: { const qc = useQueryClient() const suppliers = useQuery({ queryKey: ['all-suppliers'], - queryFn: async () => (await api.get<{ items: Supplier[] }>('/suppliers', { params: { pageSize: 1000 } })).data.items, + queryFn: async () => (await api.get<{ items: Supplier[] }>('/suppliers', { params: { pageSize: 1000, published: true } })).data.items, // S113 R4: chỉ NCC đã công bố (ẩn nháp) }) const [form, setForm] = useState({ supplierId: '', diff --git a/fe-user/src/pages/contracts/ContractCreatePage.tsx b/fe-user/src/pages/contracts/ContractCreatePage.tsx index 99553a8..5ec9061 100644 --- a/fe-user/src/pages/contracts/ContractCreatePage.tsx +++ b/fe-user/src/pages/contracts/ContractCreatePage.tsx @@ -318,7 +318,7 @@ function ContractHeaderForm({ const suppliers = useQuery({ queryKey: ['suppliers-all'], - queryFn: async () => (await api.get>('/suppliers', { params: { page: 1, pageSize: 200 } })).data.items, + queryFn: async () => (await api.get>('/suppliers', { params: { page: 1, pageSize: 200, published: true } })).data.items, // S113 R4: chỉ NCC đã công bố (ẩn nháp → mã HĐ không dính Code rỗng) }) const projects = useQuery({ queryKey: ['projects-all'], diff --git a/fe-user/src/pages/master/SuppliersPage.tsx b/fe-user/src/pages/master/SuppliersPage.tsx index 0126872..6e26f8c 100644 --- a/fe-user/src/pages/master/SuppliersPage.tsx +++ b/fe-user/src/pages/master/SuppliersPage.tsx @@ -1,6 +1,6 @@ import { useState, type FormEvent } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { Pencil, Plus, Trash2, Upload } from 'lucide-react' +import { EyeOff, Globe, Pencil, Plus, Trash2, Upload } from 'lucide-react' import { toast } from 'sonner' import { PageHeader } from '@/components/PageHeader' import { DataTable, Pagination, type Column } from '@/components/DataTable' @@ -77,11 +77,24 @@ export function SuppliersPage() { const [form, setForm] = useState(emptyForm) const isEdit = !!form.id + // Bộ lọc công bố: '' = tất cả · 'true' = đã công bố · 'false' = nháp (ẩn) + const [published, setPublished] = useState('') + // Dialog công bố: nhập Mã NCC trước khi NCC hiển thị ra ngoài + const [publishTarget, setPublishTarget] = useState(null) + const [maNcc, setMaNcc] = useState('') + const list = useQuery({ - queryKey: ['suppliers', { page, search, sortBy, sortDesc }], + queryKey: ['suppliers', { page, search, sortBy, sortDesc, published }], queryFn: async () => { const res = await api.get>('/suppliers', { - params: { page, pageSize: 20, search: search || undefined, sortBy, sortDesc }, + params: { + page, + pageSize: 20, + search: search || undefined, + sortBy, + sortDesc, + published: published === '' ? undefined : published === 'true', + }, }) return res.data }, @@ -143,6 +156,24 @@ export function SuppliersPage() { onError: err => toast.error(getErrorMessage(err)), }) + // Công bố / ẩn NCC — KHÔNG qua UpdateSupplier (chỉ đổi Mã NCC + IsPublic). + const publish = useMutation({ + mutationFn: async ({ id, maNcc, doPublish }: { id: string; maNcc?: string; doPublish: boolean }) => + await api.post(`/suppliers/${id}/publish`, { maNcc: maNcc || undefined, publish: doPublish }), + onSuccess: (_data, vars) => { + qc.invalidateQueries({ queryKey: ['suppliers'] }) + toast.success(vars.doPublish ? 'Đã công bố NCC' : 'Đã ẩn NCC') + setPublishTarget(null) + setMaNcc('') + }, + onError: err => toast.error(getErrorMessage(err)), + }) + + function openPublish(s: Supplier) { + setPublishTarget(s) + setMaNcc(s.code ?? '') + } + function openNew() { setForm(emptyForm) setOpen(true) @@ -177,13 +208,47 @@ export function SuppliersPage() { { key: 'type', header: 'Loại', sortable: true, width: 'w-36', render: s => SupplierTypeLabel[s.type] }, { key: 'taxCode', header: 'MST', render: s => s.taxCode ?? '—' }, { key: 'phone', header: 'Điện thoại', render: s => s.phone ?? '—' }, + { + key: 'isPublic', + header: 'Trạng thái', + width: 'w-32', + render: s => + s.isPublic ? ( + + Đã công bố + + ) : ( + + Nháp (ẩn) + + ), + }, { key: 'actions', header: '', align: 'right', - width: 'w-32', + width: 'w-40', render: s => (
+ + {s.isPublic ? ( + + ) : ( + + )} +
+ setPublishTarget(null)} + title="Công bố nhà cung cấp" + size="sm" + footer={ + <> + + + + } + > +
+

+ Nhập Mã NCC để công bố "{publishTarget?.name}". Sau khi công bố, NCC sẽ hiển thị ở các màn chọn + NCC (duyệt NCC, hợp đồng). +

+
+ + setMaNcc(e.target.value)} placeholder="VD: NCC001" autoFocus /> +
+

NCC chưa có Mã NCC sẽ ở trạng thái Nháp (ẩn), chưa hiển thị ra ngoài.

+
+
+ setImportOpen(false)} /> ) diff --git a/fe-user/src/types/master.ts b/fe-user/src/types/master.ts index 07163d6..dad4df6 100644 --- a/fe-user/src/types/master.ts +++ b/fe-user/src/types/master.ts @@ -75,6 +75,8 @@ export type Supplier = { ownerPmh: string | null // Tình trạng status: SupplierStatus | null + // Công bố: true = hiển thị ra ngoài (picker NCC), false = nháp/ẩn (S113) + isPublic: boolean createdAt: string updatedAt: string | null } diff --git a/src/Backend/SolutionErp.Api/Controllers/SuppliersController.cs b/src/Backend/SolutionErp.Api/Controllers/SuppliersController.cs index 9cc3ff9..c219385 100644 --- a/src/Backend/SolutionErp.Api/Controllers/SuppliersController.cs +++ b/src/Backend/SolutionErp.Api/Controllers/SuppliersController.cs @@ -5,9 +5,11 @@ using SolutionErp.Application.Common.Models; using SolutionErp.Application.Master.Suppliers.Commands.CreateSupplier; using SolutionErp.Application.Master.Suppliers.Commands.DeleteSupplier; using SolutionErp.Application.Master.Suppliers.Commands.ImportSuppliers; +using SolutionErp.Application.Master.Suppliers.Commands.PublishSupplier; using SolutionErp.Application.Master.Suppliers.Commands.UpdateSupplier; using SolutionErp.Application.Master.Suppliers.Dtos; using SolutionErp.Application.Master.Suppliers.Queries.GetSupplier; +using SolutionErp.Application.Master.Suppliers.Queries.GetSupplierImportTemplate; using SolutionErp.Application.Master.Suppliers.Queries.ListSuppliers; using SolutionErp.Domain.Master; @@ -18,13 +20,14 @@ namespace SolutionErp.Api.Controllers; [Authorize] public class SuppliersController(IMediator mediator) : ControllerBase { + // published: null = tất cả (admin quản lý thấy cả nháp) · true = chỉ NCC đã công bố (picker/consumer) · false = chỉ nháp. [HttpGet] public async Task>> List( [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? search = null, [FromQuery] string? sortBy = null, [FromQuery] bool sortDesc = true, - [FromQuery] SupplierType? type = null, + [FromQuery] SupplierType? type = null, [FromQuery] bool? published = null, CancellationToken ct = default) - => Ok(await mediator.Send(new ListSuppliersQuery(type) { Page = page, PageSize = pageSize, Search = search, SortBy = sortBy, SortDesc = sortDesc }, ct)); + => Ok(await mediator.Send(new ListSuppliersQuery(type, published) { Page = page, PageSize = pageSize, Search = search, SortBy = sortBy, SortDesc = sortDesc }, ct)); [HttpGet("{id:guid}")] public async Task> Get(Guid id, CancellationToken ct) @@ -59,9 +62,28 @@ public class SuppliersController(IMediator mediator) : ControllerBase return NoContent(); } - // ========== Import Excel "Database NCC" (Supplier Phase B, Approach A — layout-locked) ========== + // [S113 D3] Công bố / ẩn 1 NCC (import v2). NHÁP (thiếu Mã NCC) → publish tay ở đây. + // KHÔNG dùng UpdateSupplierCommand (blind absolute-set → clobber #73). Body { maNcc?, publish }. + [Authorize(Policy = "Suppliers.Update")] + [HttpPost("{id:guid}/publish")] + public async Task Publish(Guid id, [FromBody] PublishSupplierBody body, CancellationToken ct) + { + await mediator.Send(new PublishSupplierCommand(id, body.MaNcc, body.Publish), ct); + return NoContent(); + } + + // ========== Import Excel "Database NCC" (Supplier import v2 — layout-locked ROW 4, 30 cột) ========== + // [S113 D3] authz: policy quyền Suppliers Update (danh mục), khớp FE PermissionGuard (hết 403 non-admin). + // Tải file mẫu (.xlsx trống, 30 token header ROW 4). Single-source ExpectedHeaderTokens (0 drift). + [Authorize(Policy = "Suppliers.Update")] + [HttpGet("import/template")] + public async Task ImportTemplate(CancellationToken ct) + => File(await mediator.Send(new GetSupplierImportTemplateQuery(), ct), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Mau-Database-NCC.xlsx"); + // Preview: parse + phân loại từng hàng (KHÔNG ghi DB). Header sai layout → LayoutValid=false. - [Authorize(Roles = "Admin,CatalogManager")] + [Authorize(Policy = "Suppliers.Update")] [HttpPost("import/preview")] [RequestSizeLimit(25_000_000)] public async Task> ImportPreview(IFormFile file, CancellationToken ct) @@ -72,9 +94,9 @@ public class SuppliersController(IMediator mediator) : ControllerBase return Ok(await mediator.Send(new SupplierImportPreviewCommand(stream), ct)); } - // Confirm: ALL-OR-NOTHING upsert (New → insert; existing case-insensitive-Code → fill-nulls). + // Confirm: ALL-OR-NOTHING upsert (dedup MST-primary + Code-backstop; blank Code = draft ẩn). // Body JSON = { "rows": [ ... ] } (mảng SupplierImportRowDto round-trip từ preview). - [Authorize(Roles = "Admin,CatalogManager")] + [Authorize(Policy = "Suppliers.Update")] [HttpPost("import/confirm")] public async Task> ImportConfirm( [FromBody] SupplierImportConfirmCommand cmd, CancellationToken ct) diff --git a/src/Backend/SolutionErp.Application/Master/Suppliers/Commands/CreateSupplier/CreateSupplierCommand.cs b/src/Backend/SolutionErp.Application/Master/Suppliers/Commands/CreateSupplier/CreateSupplierCommand.cs index 8b0dca7..6cdb288 100644 --- a/src/Backend/SolutionErp.Application/Master/Suppliers/Commands/CreateSupplier/CreateSupplierCommand.cs +++ b/src/Backend/SolutionErp.Application/Master/Suppliers/Commands/CreateSupplier/CreateSupplierCommand.cs @@ -112,6 +112,7 @@ public class CreateSupplierCommandHandler : IRequestHandler +/// Công bố / ẩn 1 NCC (Supplier import v2, S113). KHÔNG dùng UpdateSupplierCommand (blind absolute-set +/// mọi field → clobber data nháp, gotcha #73). Chỉ set Mã NCC (nếu gửi) + IsPublic. +/// Gate D2: publish=true bắt buộc Code non-empty (thiếu Mã NCC → không công bố được). +/// +/// Supplier cần công bố/ẩn. +/// Mã NCC mới (null = giữ nguyên Code hiện tại). Nếu gửi non-empty → unique-CI-check. +/// true = công bố · false = ẩn (đưa về nháp). +public sealed record PublishSupplierCommand(Guid Id, string? MaNcc, bool Publish) : IRequest; + +/// Body cho endpoint POST /suppliers/{id}/publish (Id lấy từ route). +public sealed record PublishSupplierBody(string? MaNcc, bool Publish); + +public sealed class PublishSupplierCommandValidator : AbstractValidator +{ + public PublishSupplierCommandValidator() + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.MaNcc).MaximumLength(50); // khớp Supplier.Code HasMaxLength(50) + } +} + +public sealed class PublishSupplierCommandHandler(IApplicationDbContext db) + : IRequestHandler +{ + public async Task Handle(PublishSupplierCommand request, CancellationToken ct) + { + var entity = await db.Suppliers.FirstOrDefaultAsync(x => x.Id == request.Id, ct) + ?? throw new NotFoundException("Supplier", request.Id); + + // Đặt Mã NCC nếu gửi (blank = xóa Code → ẩn). Unique-CI-check chỉ khi non-empty + // (Code="" bị loại khỏi filtered-unique index nên không cần check). + if (request.MaNcc is not null) + { + var newCode = request.MaNcc.Trim(); + if (newCode.Length > 0 && newCode != entity.Code && + await db.Suppliers.AnyAsync(x => x.Code == newCode && x.Id != entity.Id, ct)) + throw new ConflictException($"Mã NCC '{newCode}' đã tồn tại."); + entity.Code = newCode; + } + + // Gate D2: công bố bắt buộc có Mã NCC. + if (request.Publish && string.IsNullOrWhiteSpace(entity.Code)) + throw new ConflictException("Cần Mã NCC để công bố nhà cung cấp."); + + entity.IsPublic = request.Publish; + await db.SaveChangesAsync(ct); + } +} diff --git a/src/Backend/SolutionErp.Application/Master/Suppliers/Dtos/SupplierDto.cs b/src/Backend/SolutionErp.Application/Master/Suppliers/Dtos/SupplierDto.cs index 65e742b..22fa32d 100644 --- a/src/Backend/SolutionErp.Application/Master/Suppliers/Dtos/SupplierDto.cs +++ b/src/Backend/SolutionErp.Application/Master/Suppliers/Dtos/SupplierDto.cs @@ -35,4 +35,6 @@ public record SupplierDto( DateTime? SourceUpdatedAt, string? SourceUpdatedBy, DateTime CreatedAt, - DateTime? UpdatedAt); + DateTime? UpdatedAt, + // Publish state (Mig 64, S113) — append CUỐI positional record. false = nháp/ẩn (thiếu Mã NCC). + bool IsPublic); diff --git a/src/Backend/SolutionErp.Application/Master/Suppliers/Dtos/SupplierImportDtos.cs b/src/Backend/SolutionErp.Application/Master/Suppliers/Dtos/SupplierImportDtos.cs index 0cd2b9e..dc9cc7d 100644 --- a/src/Backend/SolutionErp.Application/Master/Suppliers/Dtos/SupplierImportDtos.cs +++ b/src/Backend/SolutionErp.Application/Master/Suppliers/Dtos/SupplierImportDtos.cs @@ -11,10 +11,10 @@ namespace SolutionErp.Application.Master.Suppliers.Dtos; /// Phân loại mỗi hàng sau khi preview classify. public enum RowImportStatus { - New = 0, // Code chưa có trong DB (case-insensitive) → sẽ INSERT - Update = 1, // Code đã có (case-insensitive) → sẽ FILL-NULLS (không đè non-null) + New = 0, // MST + Code đều chưa match trong DB → sẽ INSERT (blank Code = draft ẩn) + Update = 1, // match theo MST (ưu tiên) hoặc Code → sẽ FILL-NULLS (không đè non-null) Skip = 2, // hàng rỗng hoàn toàn → bỏ qua - Error = 3, // thiếu Code hoặc Name → không import được + Error = 3, // thiếu Name → không import được (v2: blank Code KHÔNG còn là lỗi, = draft hợp lệ) } /// @@ -62,6 +62,7 @@ public sealed record SupplierImportRowDto public RowImportStatus Status { get; set; } public List Messages { get; set; } = new(); // cảnh báo truncate / default type / lý do lỗi public Guid? ExistingSupplierId { get; set; } // set khi Status=Update (hỗ trợ FE hiển thị "sẽ cập nhật") + public bool MstMissing { get; set; } // true khi cột 12 (MST) rỗng — cảnh báo mềm (không chặn), FE render ⚠️ } /// Kết quả preview (KHÔNG ghi DB). LayoutValid=false khi header-fingerprint không khớp. @@ -82,6 +83,7 @@ public sealed record SupplierImportResultDto public int Inserted { get; set; } public int Updated { get; set; } public int Skipped { get; set; } + public int DraftCount { get; set; } // số NCC MỚI lưu nháp (blank Code → IsPublic=false) trong lần import này public List Errors { get; set; } = new(); public bool Committed { get; set; } // true nếu đã SaveChanges; false nếu abort do hard-error } diff --git a/src/Backend/SolutionErp.Application/Master/Suppliers/Import/ISupplierExcelImportService.cs b/src/Backend/SolutionErp.Application/Master/Suppliers/Import/ISupplierExcelImportService.cs index eaa5e27..43be523 100644 --- a/src/Backend/SolutionErp.Application/Master/Suppliers/Import/ISupplierExcelImportService.cs +++ b/src/Backend/SolutionErp.Application/Master/Suppliers/Import/ISupplierExcelImportService.cs @@ -16,9 +16,16 @@ public interface ISupplierExcelImportService Task PreviewAsync(Stream xlsx, CancellationToken ct = default); /// - /// ALL-OR-NOTHING: re-validate; nếu có hard-error (thiếu Code/Name) → trả Errors, KHÔNG ghi gì. - /// Ngược lại upsert (New → insert; existing case-insensitive-Code → fill-nulls-only) rồi - /// SaveChanges 1 lần. chỉ dùng để log (audit thật do interceptor set). + /// ALL-OR-NOTHING: re-validate; nếu có hard-error (thiếu Name) → trả Errors, KHÔNG ghi gì. + /// Ngược lại upsert (dedup MST-primary + Code-backstop; New → insert, blank Code = draft IsPublic=false; + /// match → fill-nulls-only) rồi SaveChanges 1 lần. chỉ dùng để log + /// (audit thật do interceptor set). /// Task ConfirmAsync(IReadOnlyList rows, string actor, CancellationToken ct = default); + + /// + /// Sinh file .xlsx mẫu trống đúng layout (30 token header vào ROW 4, freeze row 4) từ + /// single-source ExpectedHeaderTokens → 0 drift với validator. Người dùng tải về, điền, upload lại. + /// + byte[] BuildTemplate(); } diff --git a/src/Backend/SolutionErp.Application/Master/Suppliers/Queries/GetSupplier/GetSupplierQuery.cs b/src/Backend/SolutionErp.Application/Master/Suppliers/Queries/GetSupplier/GetSupplierQuery.cs index 80edeb0..c277b4c 100644 --- a/src/Backend/SolutionErp.Application/Master/Suppliers/Queries/GetSupplier/GetSupplierQuery.cs +++ b/src/Backend/SolutionErp.Application/Master/Suppliers/Queries/GetSupplier/GetSupplierQuery.cs @@ -23,6 +23,6 @@ public class GetSupplierQueryHandler : IRequestHandler +/// Tải file .xlsx mẫu "Database NCC" (30 cột, header ROW 4) — Supplier import v2 (S113). +/// Bytes sinh từ ISupplierExcelImportService.BuildTemplate() (single-source token, 0 drift). +/// +public sealed record GetSupplierImportTemplateQuery : IRequest; + +public sealed class GetSupplierImportTemplateQueryHandler(ISupplierExcelImportService importService) + : IRequestHandler +{ + public Task Handle(GetSupplierImportTemplateQuery request, CancellationToken ct) + => Task.FromResult(importService.BuildTemplate()); +} diff --git a/src/Backend/SolutionErp.Application/Master/Suppliers/Queries/ListSuppliers/ListSuppliersQuery.cs b/src/Backend/SolutionErp.Application/Master/Suppliers/Queries/ListSuppliers/ListSuppliersQuery.cs index be417d7..01b562f 100644 --- a/src/Backend/SolutionErp.Application/Master/Suppliers/Queries/ListSuppliers/ListSuppliersQuery.cs +++ b/src/Backend/SolutionErp.Application/Master/Suppliers/Queries/ListSuppliers/ListSuppliersQuery.cs @@ -7,7 +7,8 @@ using SolutionErp.Domain.Master; namespace SolutionErp.Application.Master.Suppliers.Queries.ListSuppliers; -public record ListSuppliersQuery(SupplierType? Type = null) : PagedRequest, IRequest>; +// Published: null = tất cả (admin quản lý, thấy cả nháp) · true = chỉ NCC đã công bố (picker/consumer) · false = chỉ nháp. +public record ListSuppliersQuery(SupplierType? Type = null, bool? Published = null) : PagedRequest, IRequest>; public class ListSuppliersQueryHandler : IRequestHandler> { @@ -22,6 +23,12 @@ public class ListSuppliersQueryHandler : IRequestHandler x.Type == request.Type); + if (request.Published is not null) + { + var pub = request.Published.Value; // local → EF dịch được (tránh nullable bool trong expr) + query = query.Where(x => x.IsPublic == pub); + } + if (!string.IsNullOrWhiteSpace(request.Search)) { var s = request.Search.Trim(); @@ -51,7 +58,7 @@ public class ListSuppliersQueryHandler : IRequestHandler(items, total, request.Page, request.PageSize); diff --git a/src/Backend/SolutionErp.Domain/Master/Supplier.cs b/src/Backend/SolutionErp.Domain/Master/Supplier.cs index 42b2370..66c0e81 100644 --- a/src/Backend/SolutionErp.Domain/Master/Supplier.cs +++ b/src/Backend/SolutionErp.Domain/Master/Supplier.cs @@ -38,4 +38,9 @@ public class Supplier : AuditableEntity // Nguồn cập nhật từ file Excel gốc (cột 29-30), KHÁC audit CreatedAt/UpdatedBy (do hệ thống set). public DateTime? SourceUpdatedAt { get; set; } // "NGÀY CẬP NHẬT CUỐI" trên file Excel nguồn public string? SourceUpdatedBy { get; set; } // "NGƯỜI CẬP NHẬT" trên file Excel nguồn + + // ---- Publish state (Supplier import v2, Mig 64 — S113) ---- + // Cổng công bố: NCC nhập từ import thiếu Mã NCC → lưu NHÁP (IsPublic=false, ẩn khỏi picker). + // Publish qua PublishSupplierCommand (gate Code non-empty). KHÁC SupplierStatus (business-status c27). + public bool IsPublic { get; set; } // default false; backfill prod = true (Mig 64) } diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/SupplierConfiguration.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/SupplierConfiguration.cs index 20d00d4..62cfa46 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/SupplierConfiguration.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/SupplierConfiguration.cs @@ -44,7 +44,13 @@ public class SupplierConfiguration : IEntityTypeConfiguration // Import provenance (Supplier Phase B). SourceUpdatedAt = DateTime? (no length). SourceUpdatedBy = text. b.Property(x => x.SourceUpdatedBy).HasMaxLength(200); - b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0"); // Mig 47 (gotcha #57 EXT) — soft-deleted slot reusable, khớp HasQueryFilter !IsDeleted app-check + // Publish state (Mig 64, S113) — bool NOT NULL default false. Code IsRequired giữ, nhưng "" hợp lệ + // cho draft-row (import thiếu Mã NCC) → unique-index đổi filter loại "" (bên dưới). + b.Property(x => x.IsPublic).HasDefaultValue(false); + + // Mig 64: unique Code filtered `[IsDeleted]=0 AND [Code]<>''` — draft-row Code="" coexist, chỉ + // enforce unique cho Code non-empty (đã publish/publish-able). Was `[IsDeleted]=0` (Mig 47). + b.HasIndex(x => x.Code).IsUnique().HasFilter("[IsDeleted] = 0 AND [Code] <> ''"); b.HasIndex(x => x.Type); b.HasQueryFilter(x => !x.IsDeleted); diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index b294a43..902027f 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -2543,6 +2543,7 @@ public static class DbInitializer foreach (var s in suppliersToSeed) { if (existingSupplierCodes.Contains(s.Code)) continue; + s.IsPublic = true; // Mig 64 (S113) — sample NCC đều có Code → công bố db.Suppliers.Add(s); addedSuppliers++; } @@ -2811,9 +2812,11 @@ public static class DbInitializer if (e.ReferralSource is null && s.ReferralSource is not null) { e.ReferralSource = s.ReferralSource; changed = true; } if (e.OwnerPmh is null && s.OwnerPmh is not null) { e.OwnerPmh = s.OwnerPmh; changed = true; } if (e.Status is null && s.Status is not null) { e.Status = s.Status; changed = true; } + if (!e.IsPublic) { e.IsPublic = true; changed = true; } // Mig 64 (S113) — 4 NCC real live, luôn công bố if (changed) filledSuppliers++; continue; } + s.IsPublic = true; // Mig 64 (S113) — 4 NCC real đều có Code → công bố db.Suppliers.Add(s); addedSuppliers++; } diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260712110556_AddSupplierPublishState.Designer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260712110556_AddSupplierPublishState.Designer.cs new file mode 100644 index 0000000..8231419 --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260712110556_AddSupplierPublishState.Designer.cs @@ -0,0 +1,6360 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SolutionErp.Infrastructure.Persistence; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260712110556_AddSupplierPublishState")] + partial class AddSupplierPublishState + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("ApplicableType") + .HasColumnType("int"); + + b.Property("CeoApprovalThreshold") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsUserSelectable") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ApplicableType", "IsActive"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.ToTable("ApprovalWorkflows", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AllowApproverEditBudget") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowApproverEditDetails") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowApproverFinalize") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowApproverSkipToFinal") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnOneLevel") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnOneStep") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnToAssignee") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowReturnToDrafter") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("ApprovalWorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("ApprovalWorkflowStepId", "Order"); + + b.ToTable("ApprovalWorkflowLevels", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("ApprovalWorkflowId", "Order"); + + b.ToTable("ApprovalWorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetManualAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("BudgetManualName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BypassProcurementAndCCM") + .HasColumnType("bit"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("CurrentWorkflowStepIndex") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("DraftData") + .HasColumnType("nvarchar(max)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("GiaTri") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaHopDong") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("NoiDung") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("RejectedAtStepIndex") + .HasColumnType("int"); + + b.Property("RejectedFromPhase") + .HasColumnType("int"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("SupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("TenHopDong") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowId"); + + b.HasIndex("MaHopDong") + .IsUnique() + .HasFilter("[MaHopDong] IS NOT NULL"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("SupplierId"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("Contracts", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "ApprovedAt"); + + b.ToTable("ContractApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Purpose") + .HasColumnType("int"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId"); + + b.ToTable("ContractAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "CreatedAt"); + + b.HasIndex("ContractId", "EntityType"); + + b.ToTable("ContractChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("ContractCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "CreatedAt"); + + b.ToTable("ContractComments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractDepartmentApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverRoleSnapshot") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsBypassed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PhaseAtApproval") + .HasColumnType("int"); + + b.Property("Stage") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("ContractId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("ContractId", "PhaseAtApproval", "DepartmentId", "Stage") + .IsUnique() + .HasDatabaseName("UX_ContractDeptApprovals_Contract_Phase_Dept_Stage"); + + b.ToTable("ContractDepartmentApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("ContractId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("ContractLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DenNgay") + .HasColumnType("datetime2"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaDichVu") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenDichVu") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGian") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TuNgay") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("DichVuDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.GiaoKhoanDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MaCongViec") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenCongViec") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianHoanThanh") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("YeuCauKyThuat") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("GiaoKhoanDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.MuaBanDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaSP") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SoLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThueVAT") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("XuatXu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("MuaBanDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacDvDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGiaToiDa") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonGiaToiThieu") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("LoaiDichVu") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PhamViDichVu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("SLA") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TenDichVu") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NguyenTacDvDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacNccDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DieuKienGiaoHang") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DieuKienThanhToan") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("DonGiaToiDa") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonGiaToiThieu") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("NhomSP") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NguyenTacNccDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NhaCungCapDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MaSP") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("SoLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TenSP") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianGiao") + .HasColumnType("datetime2"); + + b.Property("ThongSoKyThuat") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("XuatXu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("NhaCungCapDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.ThauPhuDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGia") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("HangMuc") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("KhoiLuong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ThoiGianHoanThanh") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractId", "Order"); + + b.ToTable("ThauPhuDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.HasIndex("ContractType", "IsActive"); + + b.ToTable("WorkflowDefinitions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("PositionLevel") + .HasColumnType("int"); + + b.Property("SlaDays") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("WorkflowDefinitionId", "Order"); + + b.ToTable("WorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignmentValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("WorkflowStepId"); + + b.ToTable("WorkflowStepApprovers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowTypeAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("PolicyName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractType") + .IsUnique(); + + b.ToTable("WorkflowTypeAssignments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Forms.ContractClause", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ContractClauses", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Forms.ContractTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContractType") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FieldSpec") + .HasColumnType("nvarchar(max)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FormCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Format") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ContractType"); + + b.HasIndex("FormCode") + .IsUnique(); + + b.ToTable("ContractTemplates", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.Driver", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LicenseClass") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Drivers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("EmployeeCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeDocument", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DocumentType") + .HasColumnType("int"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("ExpiryDate") + .HasColumnType("date"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IssueDate") + .HasColumnType("date"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DocumentType"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeDocuments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeEducation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CertificateIssueDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DegreeLevel") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EducationMode") + .HasColumnType("int"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FromDate") + .HasColumnType("date"); + + b.Property("GradeLevel") + .HasColumnType("int"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Major") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SchoolName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ToDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeEducations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeFamilyRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BirthYear") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Occupation") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Relationship") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeFamilyRelations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AcademicTitle") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("AnnualLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("BankAccount") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("BankBranch") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BankName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BaseSalary") + .HasColumnType("decimal(18,2)"); + + b.Property("BirthPlace") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BloodType") + .HasMaxLength(5) + .HasColumnType("nvarchar(5)"); + + b.Property("CommunistPartyJoinDate") + .HasColumnType("date"); + + b.Property("CompensatoryLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmergencyContactAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("EmergencyContactName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("EmployeeCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("EmployeeStatus") + .HasColumnType("int"); + + b.Property("EmployeeType") + .HasColumnType("int"); + + b.Property("Ethnicity") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("HeightCm") + .HasColumnType("int"); + + b.Property("HireDate") + .HasColumnType("date"); + + b.Property("Hometown") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IdCardIssueDate") + .HasColumnType("date"); + + b.Property("IdCardIssuePlace") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IdCardNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("InternalPhone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("IsCommunistParty") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsTradeUnion") + .HasColumnType("bit"); + + b.Property("IsYouthUnion") + .HasColumnType("bit"); + + b.Property("MaritalStatus") + .HasColumnType("int"); + + b.Property("MedicalRegistrationPlace") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Nationality") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PassportNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("PermanentAddressText") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PermanentDistrictId") + .HasColumnType("uniqueidentifier"); + + b.Property("PermanentProvinceId") + .HasColumnType("uniqueidentifier"); + + b.Property("PermanentWardId") + .HasColumnType("uniqueidentifier"); + + b.Property("PersonalEmail") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Phone") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("PhotoUrl") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Qualification") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Religion") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RemainingLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("ResignDate") + .HasColumnType("date"); + + b.Property("SeniorityLeaveDays") + .HasColumnType("decimal(5,2)"); + + b.Property("SocialInsuranceNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("SocialInsuranceStartDate") + .HasColumnType("date"); + + b.Property("StreetAddressPermanent") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StreetAddressTemporary") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("TaxCode") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("TemporaryAddressText") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("TemporaryDistrictId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemporaryProvinceId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemporaryWardId") + .HasColumnType("uniqueidentifier"); + + b.Property("TimekeepingCode") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("TotalSalary") + .HasColumnType("decimal(18,2)"); + + b.Property("TradeUnionJoinDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("WeightKg") + .HasColumnType("int"); + + b.Property("WorkLocation") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("YouthUnionJoinDate") + .HasColumnType("date"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeCode") + .IsUnique(); + + b.HasIndex("IsDeleted"); + + b.HasIndex("Phone"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("EmployeeProfiles", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeSkill", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LanguageId") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Level") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.HasIndex("Kind"); + + b.ToTable("EmployeeSkills", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeWorkHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompanyAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("FromDate") + .HasColumnType("date"); + + b.Property("Industry") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("JobDescription") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("JobTitle") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ResignReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ToDate") + .HasColumnType("date"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeProfileId"); + + b.ToTable("EmployeeWorkHistories", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.Holiday", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPaid") + .HasColumnType("bit"); + + b.Property("IsRecurring") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Year", "Date") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Holidays", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveBalance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AdjustmentDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntitledDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UsedDays") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "LeaveTypeId", "Year") + .IsUnique(); + + b.ToTable("LeaveBalances", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DaysPerYear") + .HasColumnType("decimal(5,2)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPaid") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequiresAttachment") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("LeaveTypes", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.OtPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaxHoursPerDay") + .HasColumnType("int"); + + b.Property("MaxHoursPerMonth") + .HasColumnType("int"); + + b.Property("MaxHoursPerYear") + .HasColumnType("int"); + + b.Property("MultiplierHoliday") + .HasColumnType("decimal(4,2)"); + + b.Property("MultiplierWeekday") + .HasColumnType("decimal(4,2)"); + + b.Property("MultiplierWeekend") + .HasColumnType("decimal(4,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("OtPolicies", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.ShiftPattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BreakMinutes") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("EndTime") + .HasColumnType("time"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("StartTime") + .HasColumnType("time"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkDays") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("ShiftPatterns", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.Vehicle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LicensePlate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SeatCount") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Vehicles", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.Property("Key") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DisplayLabel") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsVisible") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ParentKey") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Key"); + + b.HasIndex("ParentKey"); + + b.ToTable("MenuItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CanCreate") + .HasColumnType("bit"); + + b.Property("CanDelete") + .HasColumnType("bit"); + + b.Property("CanRead") + .HasColumnType("bit"); + + b.Property("CanUpdate") + .HasColumnType("bit"); + + b.Property("MenuKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MenuKey"); + + b.HasIndex("RoleId", "MenuKey") + .IsUnique(); + + b.ToTable("Permissions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ShortName") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("CanBypassReview") + .HasColumnType("bit"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("Position") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PositionLevel") + .HasColumnType("int"); + + b.Property("RefreshToken") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("datetime2"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.MaterialItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OriginCountry") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Specification") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("MaterialItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.ServiceItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("ServiceItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.UnitOfMeasure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("UnitsOfMeasure", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Catalogs.WorkItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DefaultUnit") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("WorkItems", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ManagerUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ParentId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.HasIndex("ParentId"); + + b.ToTable("Departments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BudgetTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("Investor") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ManagerUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Package") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Master.Supplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("AuthorizationNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("BankAccount") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ContactPerson") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactPhone") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ContactTitle") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Fax") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsPublic") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LegalRepTitle") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("LegalRepresentative") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("LinkGpkd") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("LinkGuq") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("LinkHsnl") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("MailRecipient") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("MailingAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Note") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("OfficeAddress") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("OwnerPmh") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("PackageCategory") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("ReferralSource") + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("SecondaryBankAccount") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("SourceUpdatedAt") + .HasColumnType("datetime2"); + + b.Property("SourceUpdatedBy") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TaxCode") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsDeleted] = 0 AND [Code] <> ''"); + + b.HasIndex("Type"); + + b.ToTable("Suppliers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Notifications.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Href") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("ReadAt") + .HasColumnType("datetime2"); + + b.Property("RefId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("UserId", "ReadAt"); + + b.ToTable("Notifications", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.Attendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AttendanceDate") + .HasColumnType("datetime2"); + + b.Property("CheckInAccuracy") + .HasColumnType("decimal(8,2)"); + + b.Property("CheckInAt") + .HasColumnType("datetime2"); + + b.Property("CheckInLatitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CheckInLongitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CheckOutAccuracy") + .HasColumnType("decimal(8,2)"); + + b.Property("CheckOutAt") + .HasColumnType("datetime2"); + + b.Property("CheckOutLatitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CheckOutLongitude") + .HasColumnType("decimal(10,7)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IpAddressIn") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IpAddressOut") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("OtHours") + .HasColumnType("decimal(5,2)"); + + b.Property("SourceIn") + .HasColumnType("int"); + + b.Property("SourceOut") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkHours") + .HasColumnType("decimal(5,2)"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "AttendanceDate") + .IsUnique(); + + b.ToTable("Attendances", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ItTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignedToFullName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("AssignedToUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaTicket") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Resolution") + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("ResolvedAt") + .HasColumnType("datetime2"); + + b.Property("SlaBreached") + .HasColumnType("bit"); + + b.Property("SlaDueAt") + .HasColumnType("datetime2"); + + b.Property("SlaWarnedSent") + .HasColumnType("bit"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("AssignedToUserId"); + + b.HasIndex("Category"); + + b.HasIndex("MaTicket") + .IsUnique() + .HasFilter("[MaTicket] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("ItTickets", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NumDays") + .HasColumnType("decimal(5,2)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("LeaveRequests", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequestLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LeaveRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("LeaveRequestId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("LeaveRequestLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BookedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("BookedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EndAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("RoomId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("BookedByUserId"); + + b.HasIndex("RoomId", "StartAt"); + + b.ToTable("MeetingBookings", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBookingAttendee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BookingId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Notes") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("BookingId", "UserId") + .IsUnique(); + + b.ToTable("MeetingBookingAttendees", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingRoom", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Capacity") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Equipment") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Location") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("MeetingRooms", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("time"); + + b.Property("Hours") + .HasColumnType("decimal(5,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("OtDate") + .HasColumnType("datetime2"); + + b.Property("OtPolicyId") + .HasColumnType("uniqueidentifier"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartTime") + .HasColumnType("time"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("OtRequests", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequestLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("OtRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("OtRequestId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("OtRequestLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.Proposal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AmountEstimate") + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDeXuat") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("DrafterUserId"); + + b.HasIndex("MaDeXuat") + .IsUnique() + .HasFilter("[MaDeXuat] IS NOT NULL"); + + b.HasIndex("Status"); + + b.ToTable("Proposals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FilePath") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MimeType") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ProposalId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UploadedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UploadedByUserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ProposalId"); + + b.ToTable("ProposalAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("ProposalCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProposalId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("ProposalId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("ProposalLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("EstimatedCost") + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("NumDays") + .HasColumnType("int"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.ToTable("TravelRequests", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequestLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("TravelRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("TravelRequestId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("TravelRequestLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBooking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("DriverName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("EndAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MaDonTu") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("RejectedFromStatus") + .HasColumnType("int"); + + b.Property("RequesterFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RequesterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("VehicleLicense") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("VehicleName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("MaDonTu") + .IsUnique() + .HasFilter("[MaDonTu] IS NOT NULL"); + + b.HasIndex("RequesterUserId"); + + b.HasIndex("Status"); + + b.HasIndex("VehicleLicense", "StartAt"); + + b.ToTable("VehicleBookings", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBookingLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("VehicleBookingId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("VehicleBookingId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("VehicleBookingLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.WorkflowAppCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("WorkflowAppCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PeWorkItemBudget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AdjustmentAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CcmNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("InitialAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProAdjustmentAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProEstimateAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProInitialAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkItemId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("WorkItemId"); + + b.HasIndex("ProjectId", "WorkItemId") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("PeWorkItemBudgets", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowId") + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedPriceAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ApprovedPriceSource") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("BudgetPeriodAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CcmBudgetPeriodAmount") + .HasColumnType("decimal(18,2)"); + + b.Property("CcmSuggestedPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CcmSuggestedPriceNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ContractId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentApprovalLevelOrder") + .HasColumnType("int"); + + b.Property("CurrentWorkflowStepIndex") + .HasColumnType("int"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("DiaDiem") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("DrafterUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("EndedByLevelFinalize") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ExpectedRemainingAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("HoSoLink") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("IsUrgentByCcm") + .HasColumnType("bit"); + + b.Property("IsUrgentByPro") + .HasColumnType("bit"); + + b.Property("MaPhieu") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MoTa") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PaymentTerms") + .HasColumnType("nvarchar(max)"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("ProSuggestedMaxPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProSuggestedMinPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProSuggestedPriceNote") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("RejectedAtStepIndex") + .HasColumnType("int"); + + b.Property("RejectedFromPhase") + .HasColumnType("int"); + + b.Property("SelectedSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDeadline") + .HasColumnType("datetime2"); + + b.Property("SlaWarningSent") + .HasColumnType("bit"); + + b.Property("TenGoiThau") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("WorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowId"); + + b.HasIndex("ContractId"); + + b.HasIndex("MaPhieu") + .IsUnique() + .HasFilter("[MaPhieu] IS NOT NULL"); + + b.HasIndex("ProjectId"); + + b.HasIndex("SlaDeadline"); + + b.HasIndex("WorkItemId"); + + b.HasIndex("WorkflowDefinitionId"); + + b.HasIndex("Phase", "IsDeleted"); + + b.ToTable("PurchaseEvaluations", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Decision") + .HasColumnType("int"); + + b.Property("FromPhase") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ToPhase") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "ApprovedAt"); + + b.ToTable("PurchaseEvaluationApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("Purpose") + .HasColumnType("int"); + + b.Property("StoragePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationSupplierId"); + + b.ToTable("PurchaseEvaluationAttachments", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .HasColumnType("int"); + + b.Property("ContextNote") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("FieldChangesJson") + .HasColumnType("nvarchar(max)"); + + b.Property("PhaseAtChange") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "CreatedAt"); + + b.HasIndex("PurchaseEvaluationId", "EntityType"); + + b.ToTable("PurchaseEvaluationChangelogs", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationCodeSequence", b => + { + b.Property("Prefix") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastSeq") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Prefix"); + + b.ToTable("PurchaseEvaluationCodeSequences", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentApproval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovedAt") + .HasColumnType("datetime2"); + + b.Property("ApproverRoleSnapshot") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ApproverUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsBypassed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PhaseAtApproval") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Stage") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationId", "PhaseAtApproval", "DepartmentId", "Stage") + .IsUnique() + .HasDatabaseName("UX_PEDeptApprovals_PE_Phase_Dept_Stage"); + + b.ToTable("PurchaseEvaluationDepartmentApprovals", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("Opinion") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "Kind") + .IsUnique(); + + b.ToTable("PurchaseEvaluationDepartmentOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DonGiaNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("DonViTinh") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GhiChu") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("GroupCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ItemCode") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("KhoiLuongNganSach") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("KhoiLuongThiCong") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NoiDung") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThanhTienNganSach") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId", "Order"); + + b.ToTable("PurchaseEvaluationDetails", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalWorkflowLevelId") + .HasColumnType("uniqueidentifier"); + + b.Property("Comment") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetime2"); + + b.Property("DeletedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SignedAt") + .HasColumnType("datetime2"); + + b.Property("SignedByFullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SignedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalWorkflowLevelId"); + + b.HasIndex("PurchaseEvaluationId", "ApprovalWorkflowLevelId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationLevelOpinions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BgVat") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ChuaVat") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("IsSelected") + .HasColumnType("bit"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PurchaseEvaluationDetailId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PurchaseEvaluationSupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("ThanhTien") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationId"); + + b.HasIndex("PurchaseEvaluationSupplierId"); + + b.HasIndex("PurchaseEvaluationDetailId", "PurchaseEvaluationSupplierId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationQuotes", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContactEmail") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ContactPhone") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsWinner") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PaymentTermText") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PurchaseEvaluationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SupplierId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("SupplierId"); + + b.HasIndex("PurchaseEvaluationId", "SupplierId") + .IsUnique(); + + b.ToTable("PurchaseEvaluationSuppliers", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActivatedAt") + .HasColumnType("datetime2"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("EvaluationType") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Code", "Version") + .IsUnique(); + + b.HasIndex("EvaluationType", "IsActive"); + + b.ToTable("PurchaseEvaluationWorkflowDefinitions", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("DepartmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Phase") + .HasColumnType("int"); + + b.Property("PositionLevel") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationWorkflowDefinitionId") + .HasColumnType("uniqueidentifier"); + + b.Property("SlaDays") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("PurchaseEvaluationWorkflowDefinitionId", "Order"); + + b.ToTable("PurchaseEvaluationWorkflowSteps", (string)null); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignmentValue") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("uniqueidentifier"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("PurchaseEvaluationWorkflowStepId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("PurchaseEvaluationWorkflowStepId"); + + b.ToTable("PurchaseEvaluationWorkflowStepApprovers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("SolutionErp.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("SolutionErp.Domain.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", "Step") + .WithMany("Levels") + .HasForeignKey("ApprovalWorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.User", null) + .WithMany() + .HasForeignKey("ApproverUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", "ApprovalWorkflow") + .WithMany("Steps") + .HasForeignKey("ApprovalWorkflowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ApprovalWorkflow"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null) + .WithMany() + .HasForeignKey("ApprovalWorkflowId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Approvals") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractAttachment", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Attachments") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractChangelog", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Changelogs") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractComment", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("Comments") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractDepartmentApproval", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("DepartmentApprovals") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("LevelOpinions") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + + b.Navigation("Level"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.DichVuDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("DichVuDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.GiaoKhoanDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("GiaoKhoanDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.MuaBanDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("MuaBanDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacDvDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NguyenTacDvDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NguyenTacNccDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NguyenTacNccDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.NhaCungCapDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("NhaCungCapDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Details.ThauPhuDetail", b => + { + b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract") + .WithMany("ThauPhuDetails") + .HasForeignKey("ContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contract"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SolutionErp.Domain.Contracts.WorkflowDefinition", "WorkflowDefinition") + .WithMany("Steps") + .HasForeignKey("WorkflowDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WorkflowDefinition"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStepApprover", b => + { + b.HasOne("SolutionErp.Domain.Contracts.WorkflowStep", "Step") + .WithMany("Approvers") + .HasForeignKey("WorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeDocument", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("Documents") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeEducation", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("Educations") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeFamilyRelation", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("FamilyRelations") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeProfile", b => + { + b.HasOne("SolutionErp.Domain.Identity.User", "User") + .WithOne() + .HasForeignKey("SolutionErp.Domain.Hrm.EmployeeProfile", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeSkill", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("Skills") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeWorkHistory", b => + { + b.HasOne("SolutionErp.Domain.Hrm.EmployeeProfile", "EmployeeProfile") + .WithMany("WorkHistories") + .HasForeignKey("EmployeeProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeProfile"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.LeaveBalance", b => + { + b.HasOne("SolutionErp.Domain.Hrm.LeaveType", "LeaveType") + .WithMany() + .HasForeignKey("LeaveTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("LeaveType"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Parent") + .WithMany("Children") + .HasForeignKey("ParentKey") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.Permission", b => + { + b.HasOne("SolutionErp.Domain.Identity.MenuItem", "Menu") + .WithMany("Permissions") + .HasForeignKey("MenuKey") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Identity.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Menu"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.User", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequestLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.LeaveRequest", "LeaveRequest") + .WithMany("LevelOpinions") + .HasForeignKey("LeaveRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LeaveRequest"); + + b.Navigation("Level"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b => + { + b.HasOne("SolutionErp.Domain.Office.MeetingRoom", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBookingAttendee", b => + { + b.HasOne("SolutionErp.Domain.Office.MeetingBooking", "Booking") + .WithMany("Attendees") + .HasForeignKey("BookingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Booking"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequestLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.OtRequest", "OtRequest") + .WithMany("LevelOpinions") + .HasForeignKey("OtRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("OtRequest"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalAttachment", b => + { + b.HasOne("SolutionErp.Domain.Office.Proposal", "Proposal") + .WithMany("Attachments") + .HasForeignKey("ProposalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Proposal"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.ProposalLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.Proposal", "Proposal") + .WithMany("LevelOpinions") + .HasForeignKey("ProposalId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("Proposal"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequestLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.TravelRequest", "TravelRequest") + .WithMany("LevelOpinions") + .HasForeignKey("TravelRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("TravelRequest"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBookingLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.Office.VehicleBooking", "VehicleBooking") + .WithMany("LevelOpinions") + .HasForeignKey("VehicleBookingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("VehicleBooking"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null) + .WithMany() + .HasForeignKey("ApprovalWorkflowId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Approvals") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Attachments") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Changelogs") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentApproval", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("DepartmentApprovals") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDepartmentOpinion", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("DepartmentOpinions") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Details") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationLevelOpinion", b => + { + b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level") + .WithMany() + .HasForeignKey("ApprovalWorkflowLevelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("LevelOpinions") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", "Detail") + .WithMany("Quotes") + .HasForeignKey("PurchaseEvaluationDetailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", null) + .WithMany("Quotes") + .HasForeignKey("PurchaseEvaluationId"); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", "Supplier") + .WithMany() + .HasForeignKey("PurchaseEvaluationSupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Detail"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") + .WithMany("Suppliers") + .HasForeignKey("PurchaseEvaluationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PurchaseEvaluation"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.HasOne("SolutionErp.Domain.Master.Department", null) + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", "Definition") + .WithMany("Steps") + .HasForeignKey("PurchaseEvaluationWorkflowDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Definition"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b => + { + b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", "Step") + .WithMany("Approvers") + .HasForeignKey("PurchaseEvaluationWorkflowStepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Step"); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b => + { + b.Navigation("Levels"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => + { + b.Navigation("Approvals"); + + b.Navigation("Attachments"); + + b.Navigation("Changelogs"); + + b.Navigation("Comments"); + + b.Navigation("DepartmentApprovals"); + + b.Navigation("DichVuDetails"); + + b.Navigation("GiaoKhoanDetails"); + + b.Navigation("LevelOpinions"); + + b.Navigation("MuaBanDetails"); + + b.Navigation("NguyenTacDvDetails"); + + b.Navigation("NguyenTacNccDetails"); + + b.Navigation("NhaCungCapDetails"); + + b.Navigation("ThauPhuDetails"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowDefinition", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Contracts.WorkflowStep", b => + { + b.Navigation("Approvers"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Hrm.EmployeeProfile", b => + { + b.Navigation("Documents"); + + b.Navigation("Educations"); + + b.Navigation("FamilyRelations"); + + b.Navigation("Skills"); + + b.Navigation("WorkHistories"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Identity.MenuItem", b => + { + b.Navigation("Children"); + + b.Navigation("Permissions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.LeaveRequest", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b => + { + b.Navigation("Attendees"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.OtRequest", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.Proposal", b => + { + b.Navigation("Attachments"); + + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.TravelRequest", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.Office.VehicleBooking", b => + { + b.Navigation("LevelOpinions"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b => + { + b.Navigation("Approvals"); + + b.Navigation("Attachments"); + + b.Navigation("Changelogs"); + + b.Navigation("DepartmentApprovals"); + + b.Navigation("DepartmentOpinions"); + + b.Navigation("Details"); + + b.Navigation("LevelOpinions"); + + b.Navigation("Quotes"); + + b.Navigation("Suppliers"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b => + { + b.Navigation("Quotes"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b => + { + b.Navigation("Approvers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260712110556_AddSupplierPublishState.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260712110556_AddSupplierPublishState.cs new file mode 100644 index 0000000..e8f932b --- /dev/null +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260712110556_AddSupplierPublishState.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SolutionErp.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddSupplierPublishState : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Suppliers_Code", + table: "Suppliers"); + + migrationBuilder.AddColumn( + name: "IsPublic", + table: "Suppliers", + type: "bit", + nullable: false, + defaultValue: false); + + // Backfill: NCC hiện có (22 prod) đang live — PE/Contract tham chiếu → công bố hết + // (nếu để nháp sẽ ẩn NCC đang dùng + vỡ picker). Idempotent (set 1 mọi hàng). + migrationBuilder.Sql("UPDATE Suppliers SET IsPublic = 1;"); + + migrationBuilder.CreateIndex( + name: "IX_Suppliers_Code", + table: "Suppliers", + column: "Code", + unique: true, + filter: "[IsDeleted] = 0 AND [Code] <> ''"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Suppliers_Code", + table: "Suppliers"); + + migrationBuilder.DropColumn( + name: "IsPublic", + table: "Suppliers"); + + migrationBuilder.CreateIndex( + name: "IX_Suppliers_Code", + table: "Suppliers", + column: "Code", + unique: true, + filter: "[IsDeleted] = 0"); + } + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index cdb22eb..2f2223c 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -3266,6 +3266,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations b.Property("IsDeleted") .HasColumnType("bit"); + b.Property("IsPublic") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + b.Property("LegalRepTitle") .HasMaxLength(150) .HasColumnType("nvarchar(150)"); @@ -3354,7 +3359,7 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations b.HasIndex("Code") .IsUnique() - .HasFilter("[IsDeleted] = 0"); + .HasFilter("[IsDeleted] = 0 AND [Code] <> ''"); b.HasIndex("Type"); diff --git a/src/Backend/SolutionErp.Infrastructure/Services/SupplierExcelImportService.cs b/src/Backend/SolutionErp.Infrastructure/Services/SupplierExcelImportService.cs index 3e4a148..8720af2 100644 --- a/src/Backend/SolutionErp.Infrastructure/Services/SupplierExcelImportService.cs +++ b/src/Backend/SolutionErp.Infrastructure/Services/SupplierExcelImportService.cs @@ -32,39 +32,41 @@ public sealed class SupplierExcelImportService( // trong repo lúc scaffold). Đây là BEST-GUESS theo mapping cột. Khi upload thật lần đầu mà báo // "Sai layout", COPY chuỗi "Header nhận được" trong Warnings dán vào đây (mỗi token 1 cột, đúng // 30 phần tử). NormalizeHeader() sẽ upper + gộp-space nên viết thường/HOA đều được. + // 30 token row-4 BYTE-EXACT theo file "Database NCC" thật (S113 re-bake, spec §②). Giữ NGUYÊN + // ký tự \n trong ô + trailing-space (c7) → single-source cho CẢ validator (NormalizeHeader collapse + // \n→space) LẪN BuildTemplate() (ghi \n vào cell = header xuống dòng giống hệt file gốc, 0 drift). private static readonly string[] ExpectedHeaderTokens = { - // 30 token row-4 THẬT của file "Database NCC" (bake close-review S112 = RealFileHeaderTokens trong test). - "STT", // 1 - "GÓI THẦU", // 2 → PackageCategory - "PHÂN LOẠI (NTP/NCC/Cả hai)", // 3 → Type - "TÊN VIẾT TẮT (Dùng trong HĐ)", // 4 → Code (upsert key) - "TÊN CÔNG TY (Đầy đủ, đúng pháp lý)", // 5 → Name - "ĐỊA CHỈ XUẤT HÓA ĐƠN (Địa chỉ đăng ký kinh doanh)", // 6 → Address - "ĐỊA CHỈ VĂN PHÒNG (nếu có)", // 7 → OfficeAddress - "SỐ ĐIỆN THOẠI CÔNG TY", // 8 → Phone - "FAX", // 9 → Fax - "SỐ TÀI KHOẢN+ TÊN+CN. NGÂN HÀNG (Đầy đủ, đúng pháp lý)", // 10 → BankAccount - "SỐ TK PHỤ (nếu có)", // 11 → SecondaryBankAccount - "MÃ SỐ THUẾ", // 12 → TaxCode - "NGƯỜI ĐẠI DIỆN PHÁP LUẬT", // 13 → LegalRepresentative - "CHỨC VỤ ĐẠI DIỆN", // 14 → LegalRepTitle - "GIẤY ỦY QUYỀN (số, ngày, người ủy quyền)", // 15 → AuthorizationNote - "Link GUQ", // 16 → LinkGuq - "Link GPKD", // 17 → LinkGpkd - "Link HSNL", // 18 → LinkHsnl - "NGƯỜI LIÊN HỆ CHÍNH", // 19 → ContactPerson - "CHỨC VỤ NGƯỜI LH", // 20 → ContactTitle - "SĐT CHÍNH", // 21 → ContactPhone - "EMAIL", // 22 → Email - "ĐỊA CHỈ GỬI THƯ", // 23 → MailingAddress - "NGƯỜI NHẬN THƯ/ SDT", // 24 → MailRecipient - "NGUỒN GIỚI THIỆU", // 25 → ReferralSource - "NGƯỜI PHỤ TRÁCH (PMH)", // 26 → OwnerPmh - "TÌNH TRẠNG HIỆN TẠI", // 27 → Status - "GHI CHÚ / LÝ DO BLACKLIST", // 28 → Note - "NGÀY CẬP NHẬT CUỐI", // 29 → SourceUpdatedAt - "NGƯỜI CẬP NHẬT", // 30 → SourceUpdatedBy + "STT", // 1 + "GÓI THẦU", // 2 → PackageCategory + "PHÂN LOẠI\n(NTP/NCC/Cả hai)", // 3 → Type + "TÊN VIẾT TẮT\n(Dùng trong HĐ)", // 4 → Code (upsert key) + "TÊN CÔNG TY\n(Đầy đủ, đúng pháp lý)", // 5 → Name + "ĐỊA CHỈ XUẤT HÓA ĐƠN\n(Địa chỉ đăng ký kinh doanh)", // 6 → Address + "ĐỊA CHỈ VĂN PHÒNG \n(nếu có)", // 7 → OfficeAddress (trailing-space trước \n) + "SỐ ĐIỆN THOẠI\nCÔNG TY", // 8 → Phone + "FAX", // 9 → Fax + "SỐ TÀI KHOẢN+ TÊN+CN. NGÂN HÀNG\n(Đầy đủ, đúng pháp lý)", // 10 → BankAccount + "SỐ TK PHỤ\n(nếu có)", // 11 → SecondaryBankAccount + "MÃ SỐ THUẾ", // 12 → TaxCode + "NGƯỜI ĐẠI DIỆN\nPHÁP LUẬT", // 13 → LegalRepresentative + "CHỨC VỤ\nĐẠI DIỆN", // 14 → LegalRepTitle + "GIẤY ỦY QUYỀN\n(số, ngày, người ủy quyền)", // 15 → AuthorizationNote + "Link GUQ", // 16 → LinkGuq + "Link GPKD", // 17 → LinkGpkd + "Link HSNL", // 18 → LinkHsnl + "NGƯỜI LIÊN HỆ\nCHÍNH", // 19 → ContactPerson + "CHỨC VỤ\nNGƯỜI LH", // 20 → ContactTitle + "SĐT CHÍNH", // 21 → ContactPhone + "EMAIL", // 22 → Email + "ĐỊA CHỈ GỬI THƯ", // 23 → MailingAddress + "NGƯỜI NHẬN THƯ/\nSDT", // 24 → MailRecipient + "NGUỒN\nGIỚI THIỆU", // 25 → ReferralSource + "NGƯỜI PHỤ TRÁCH\n(PMH)", // 26 → OwnerPmh + "TÌNH TRẠNG\nHIỆN TẠI", // 27 → Status + "GHI CHÚ / LÝ DO\nBLACKLIST", // 28 → Note + "NGÀY CẬP NHẬT\nCUỐI", // 29 → SourceUpdatedAt + "NGƯỜI CẬP NHẬT", // 30 → SourceUpdatedBy }; private static readonly HashSet ErrorLiterals = new(StringComparer.OrdinalIgnoreCase) @@ -90,28 +92,44 @@ public sealed class SupplierExcelImportService( var preview = new SupplierImportPreviewDto { Warnings = warnings, LayoutValid = layoutValid }; if (!layoutValid) return preview; // file bị từ chối (sai layout) — Rows rỗng - // Load TẤT CẢ NCC hiện có → CI dict (collation-independent, dataset nhỏ ~vài chục). Chống critical-bug. + // Load TẤT CẢ NCC hiện có → 2 CI dict (MST + Code). Dataset nhỏ ~vài chục, collation-independent. var existing = await db.Suppliers.AsNoTracking().ToListAsync(ct); var existingByCode = BuildCiIndex(existing); + var existingByMst = BuildMstIndex(existing); foreach (var row in rows) { if (IsRowEmpty(row)) { row.Status = RowImportStatus.Skip; continue; } - var codeKey = row.Code?.Trim(); - var nameBlank = string.IsNullOrWhiteSpace(row.Name); - if (string.IsNullOrWhiteSpace(codeKey) || nameBlank) + // v2: CHỈ Name bắt buộc. Blank Code KHÔNG còn là lỗi (= draft ẩn, publish tay sau). + if (string.IsNullOrWhiteSpace(row.Name)) { row.Status = RowImportStatus.Error; - if (string.IsNullOrWhiteSpace(codeKey)) row.Messages.Add("Thiếu 'Tên viết tắt' (Code) — bắt buộc."); - if (nameBlank) row.Messages.Add("Thiếu 'Tên nhà cung cấp' (Name) — bắt buộc."); + row.Messages.Add("Thiếu 'Tên nhà cung cấp' (Name) — bắt buộc."); continue; } - if (existingByCode.TryGetValue(codeKey, out var ex)) + var mstKey = NormalizeMst(row.TaxCode); + if (mstKey is null) + { + row.MstMissing = true; // cảnh báo MỀM — KHÔNG chặn (R6 mềm) + row.Messages.Add("⚠️ Chưa có MST — không kiểm tra được trùng, có thể bị trùng."); + } + + var codeKey = string.IsNullOrWhiteSpace(row.Code) ? null : row.Code!.Trim(); + if (codeKey is null) + row.Messages.Add("Chưa nhập Mã NCC — sẽ lưu nháp (ẩn), công bố sau."); + + // Dedup precedence: MST (ưu tiên) → Code (backstop) → New. + if (mstKey is not null && existingByMst.TryGetValue(mstKey, out var exM)) { row.Status = RowImportStatus.Update; - row.ExistingSupplierId = ex.Id; + row.ExistingSupplierId = exM.Id; + } + else if (codeKey is not null && existingByCode.TryGetValue(codeKey, out var exC)) + { + row.Status = RowImportStatus.Update; + row.ExistingSupplierId = exC.Id; } else { @@ -135,65 +153,93 @@ public sealed class SupplierExcelImportService( { var result = new SupplierImportResultDto(); - // Pass 1 — hard-error scan (Code/Name). Hàng rỗng = skip (không lỗi). Bất kỳ hard-error → abort. + // Pass 1 — hard-error scan (CHỈ Name; v2 blank Code = draft hợp lệ). Hàng rỗng = skip. Bất kỳ lỗi → abort. var hardErrors = new List(); foreach (var row in rows) { if (IsRowEmpty(row)) continue; - var missing = new List(); - if (string.IsNullOrWhiteSpace(row.Code)) missing.Add("Tên viết tắt"); - if (string.IsNullOrWhiteSpace(row.Name)) missing.Add("Tên nhà cung cấp"); - if (missing.Count > 0) hardErrors.Add($"Dòng {row.RowIndex}: thiếu {string.Join(" + ", missing)}."); + if (string.IsNullOrWhiteSpace(row.Name)) + hardErrors.Add($"Dòng {row.RowIndex}: thiếu Tên nhà cung cấp."); } if (hardErrors.Count > 0) { result.Errors = hardErrors; - result.Committed = false; // commit nothing + result.Committed = false; // commit nothing (all-or-nothing) return result; } - // Load existing TRACKED (để fill-nulls mutate trực tiếp). Query filter loại IsDeleted. + // Load existing TRACKED (fill-nulls mutate trực tiếp). Query filter loại IsDeleted. 2 index: MST + Code. var existing = await db.Suppliers.ToListAsync(ct); var existingByCode = BuildCiIndex(existing); + var existingByMst = BuildMstIndex(existing); var batchByCode = new Dictionary(StringComparer.OrdinalIgnoreCase); - var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var batchByMst = new Dictionary(StringComparer.OrdinalIgnoreCase); + var countedUpdates = new HashSet(); // existing đã tính Updated → tránh đếm 2 lần khi 2 dòng cùng match foreach (var row in rows) { if (IsRowEmpty(row)) { result.Skipped++; continue; } - NormalizeLengths(row); // safety-net: clamp về maxlen (chống 500 nếu client bỏ qua preview) - var key = row.Code!.Trim(); + NormalizeLengths(row); // safety-net: clamp maxlen (chống 500 nếu client bỏ qua preview) + var codeKey = string.IsNullOrWhiteSpace(row.Code) ? null : row.Code!.Trim(); + var mstKey = NormalizeMst(row.TaxCode); - if (existingByCode.TryGetValue(key, out var ex)) + // Resolve target theo precedence: existing-MST → existing-Code → batch-MST → batch-Code. + Supplier? target = null; + var isExisting = false; + if (mstKey is not null && existingByMst.TryGetValue(mstKey, out var exM)) { target = exM; isExisting = true; } + else if (codeKey is not null && existingByCode.TryGetValue(codeKey, out var exC)) { target = exC; isExisting = true; } + else if (mstKey is not null && batchByMst.TryGetValue(mstKey, out var bM)) { target = bM; } + else if (codeKey is not null && batchByCode.TryGetValue(codeKey, out var bC)) { target = bC; } + + if (target is not null) { - FillNulls(ex, row); - if (seen.Add(key)) result.Updated++; - else result.Skipped++; // Code trùng lần 2 trong file → gộp fill-nulls, không đếm lại - continue; - } - if (batchByCode.TryGetValue(key, out var added)) - { - FillNulls(added, row); - result.Skipped++; // Code trùng bản ghi vừa thêm trong batch + FillNulls(target, row); // #73-safe: chỉ điền field đang null, KHÔNG đè non-null (không đụng IsPublic) + if (isExisting) + { + if (countedUpdates.Add(target)) result.Updated++; + else result.Skipped++; // existing đã tính Updated ở dòng trước → gộp, không đếm lại + } + else result.Skipped++; // gộp vào bản ghi vừa thêm trong batch continue; } + // New. Blank Code (thiếu Mã NCC) → lưu NHÁP IsPublic=false (R4); có Code → public-able. var entity = NewSupplier(row); + entity.IsPublic = !string.IsNullOrWhiteSpace(row.Code); db.Suppliers.Add(entity); - batchByCode[key] = entity; - seen.Add(key); + if (codeKey is not null) batchByCode[codeKey] = entity; + if (mstKey is not null) batchByMst[mstKey] = entity; // blank-MST KHÔNG vào batchByMst → 2 blank-MST = 2 insert result.Inserted++; + if (!entity.IsPublic) result.DraftCount++; } await db.SaveChangesAsync(ct); // single atomic commit (interceptor set CreatedBy/UpdatedBy) result.Committed = true; logger.LogInformation( - "Supplier Excel import by {Actor}: inserted={Inserted}, updated={Updated}, skipped={Skipped}", - actor, result.Inserted, result.Updated, result.Skipped); + "Supplier Excel import by {Actor}: inserted={Inserted}, updated={Updated}, skipped={Skipped}, draft={Draft}", + actor, result.Inserted, result.Updated, result.Skipped, result.DraftCount); return result; } + // ======================================================================== + // TEMPLATE — .xlsx mẫu trống (30 token header ROW 4, freeze row 4). Single-source ExpectedHeaderTokens (0 drift). + // ======================================================================== + public byte[] BuildTemplate() + { + using var wb = new XLWorkbook(); + var ws = wb.Worksheets.Add("Sheet1"); + for (int c = 1; c <= ColumnCount; c++) + ws.Cell(HeaderRow, c).Value = ExpectedHeaderTokens[c - 1]; // ROW 4; row 1-3 để trống (giống file gốc) + var header = ws.Row(HeaderRow); + header.Style.Font.Bold = true; + header.Style.Alignment.WrapText = true; // \n trong ô hiện xuống dòng + ws.SheetView.FreezeRows(HeaderRow); + using var ms = new MemoryStream(); + wb.SaveAs(ms); + return ms.ToArray(); + } + // ======================================================================== // PARSE // ======================================================================== @@ -413,7 +459,7 @@ public sealed class SupplierExcelImportService( private static Supplier NewSupplier(SupplierImportRowDto r) => new() { - Code = r.Code!.Trim(), // store trimmed, giữ hoa/thường gốc để hiển thị + Code = (r.Code ?? string.Empty).Trim(), // null-safe (S113): ô Mã NCC trống → parser null → "" (nháp R4), tránh NRE-abort-batch; hoa/thường gốc giữ Name = r.Name!, Type = r.Type, TaxCode = r.TaxCode, @@ -450,12 +496,34 @@ public sealed class SupplierExcelImportService( foreach (var s in suppliers) { var key = s.Code?.Trim(); - if (string.IsNullOrEmpty(key)) continue; + if (string.IsNullOrEmpty(key)) continue; // Code="" (draft) KHÔNG index → không dedup theo Code rỗng dict[key] = s; // last-wins; dup CI không xảy ra do filtered-unique index } return dict; } + // MST index cho dedup MST-primary (R5). NormalizeMst = Trim + bỏ MỌI whitespace nội, CI. + // Blank MST → KHÔNG index (2 NCC không-MST không coi là trùng nhau). + private static Dictionary BuildMstIndex(IEnumerable suppliers) + { + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var s in suppliers) + { + var key = NormalizeMst(s.TaxCode); + if (key is null) continue; + dict[key] = s; // last-wins (TaxCode KHÔNG có unique constraint — dup hiếm, không crash) + } + return dict; + } + + // Chuẩn hóa MST: "0312 251 859" → "0312251859" (khớp dù file ghi có/không dấu cách). null nếu rỗng. + private static string? NormalizeMst(string? raw) + { + if (string.IsNullOrWhiteSpace(raw)) return null; + var stripped = Regex.Replace(raw, @"\s+", ""); + return stripped.Length == 0 ? null : stripped; + } + private static bool IsRowEmpty(SupplierImportRowDto r) => string.IsNullOrWhiteSpace(r.Code) && string.IsNullOrWhiteSpace(r.Name) && string.IsNullOrWhiteSpace(r.PackageCategory) && string.IsNullOrWhiteSpace(r.Address) && diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/SupplierPublishAndListTests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/SupplierPublishAndListTests.cs new file mode 100644 index 0000000..2716a2d --- /dev/null +++ b/tests/SolutionErp.Infrastructure.Tests/Application/SupplierPublishAndListTests.cs @@ -0,0 +1,155 @@ +using Microsoft.EntityFrameworkCore; +using SolutionErp.Application.Common.Exceptions; +using SolutionErp.Application.Master.Suppliers.Commands.PublishSupplier; +using SolutionErp.Application.Master.Suppliers.Queries.GetSupplier; +using SolutionErp.Application.Master.Suppliers.Queries.ListSuppliers; +using SolutionErp.Domain.Master; +using SolutionErp.Infrastructure.Tests.Common; + +namespace SolutionErp.Infrastructure.Tests.Application; + +// ============================================================================ +// Supplier import v2 (S113) — PublishSupplierCommand + ListSuppliers/GetSupplier IsPublic. +// Publish-gate = SECURITY/correctness (cổng công bố NCC) → test-before-merge (docs/rules.md §7). +// Test theo CODE trên đĩa (S34) — handlers chỉ cần IApplicationDbContext → new trực tiếp với fix.Db. +// +// 🔑 PublishSupplierCommand (KHÔNG dùng UpdateSupplier, tránh #73 clobber): +// - MaNcc gửi (non-null): set Code=Trim; unique-check `x.Code == newCode && x.Id != id` (khi len>0 +// & khác Code cũ) → ConflictException nếu trùng. +// - Gate D2: Publish=true ∧ Code rỗng → ConflictException "Cần Mã NCC...". +// - IsPublic = Publish. +// +// ⚠️ Unique-check publish dùng `x.Code == newCode` (DB collation), KHÁC import service +// (OrdinalIgnoreCase in-mem). SQLite BINARY → chỉ bắt trùng ĐÚNG hoa/thường. Prod SQL Server +// CI-collation mới bắt CI-dup. Test T9b dùng trùng ĐÚNG-case (deterministic trên SQLite). +// (Observation, KHÔNG phải bug — prod collation CI xử lý; flag để biết asymmetry.) +// ============================================================================ +public class SupplierPublishAndListTests +{ + private static Supplier NewSupplier(string code, string name, bool isPublic, + SupplierType type = SupplierType.NhaCungCap) => + new() { Id = Guid.NewGuid(), Code = code, Name = name, Type = type, IsPublic = isPublic }; + + // ---- T9 — publish có Mã NCC → set Code + IsPublic=true ---- + [Fact] + public async Task PublishSupplier_WithMaNcc_SetsCode_AndIsPublicTrue() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + var draft = NewSupplier("", "Draft NCC", isPublic: false); // nháp, chưa có Mã NCC + db.Suppliers.Add(draft); + await db.SaveChangesAsync(CancellationToken.None); + + await new PublishSupplierCommandHandler(db) + .Handle(new PublishSupplierCommand(draft.Id, MaNcc: "NEW01", Publish: true), CancellationToken.None); + + var reloaded = await db.Suppliers.SingleAsync(x => x.Id == draft.Id); + reloaded.Code.Should().Be("NEW01", "MaNcc gửi → set Code"); + reloaded.IsPublic.Should().BeTrue("Publish=true + có Code → công bố"); + } + + // ---- T9b — publish với Mã NCC trùng (đúng hoa/thường) NCC khác → ConflictException ---- + [Fact] + public async Task PublishSupplier_MaNccCollidesExactCaseWithOther_ThrowsConflict_NoChange() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + var other = NewSupplier("TAKEN", "Other", isPublic: true); + var draft = NewSupplier("", "Draft", isPublic: false); + db.Suppliers.AddRange(other, draft); + await db.SaveChangesAsync(CancellationToken.None); + + var act = async () => await new PublishSupplierCommandHandler(db) + .Handle(new PublishSupplierCommand(draft.Id, MaNcc: "TAKEN", Publish: true), CancellationToken.None); + + await act.Should().ThrowAsync() + .WithMessage("*đã tồn tại*", "Mã NCC trùng NCC khác → chặn (unique-check)"); + + var reloaded = await db.Suppliers.SingleAsync(x => x.Id == draft.Id); + reloaded.Code.Should().Be("", "throw trước khi gán → draft KHÔNG đổi"); + reloaded.IsPublic.Should().BeFalse(); + } + + // ---- T10 — publish khi Code rỗng (không gửi MaNcc) → ConflictException "Cần Mã NCC" ---- + [Fact] + public async Task PublishSupplier_PublishTrue_ButCodeEmpty_ThrowsConflict_CanMaNcc() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + var draft = NewSupplier("", "Draft NCC", isPublic: false); + db.Suppliers.Add(draft); + await db.SaveChangesAsync(CancellationToken.None); + + var act = async () => await new PublishSupplierCommandHandler(db) + .Handle(new PublishSupplierCommand(draft.Id, MaNcc: null, Publish: true), CancellationToken.None); + + await act.Should().ThrowAsync() + .WithMessage("*Cần Mã NCC*", "gate D2: công bố bắt buộc có Mã NCC"); + + (await db.Suppliers.SingleAsync(x => x.Id == draft.Id)).IsPublic + .Should().BeFalse("gate chặn → vẫn nháp"); + } + + // ---- T11 — unpublish (Publish=false) LUÔN cho, IsPublic=false, giữ Code ---- + [Fact] + public async Task PublishSupplier_Unpublish_AlwaysAllowed_SetsIsPublicFalse_KeepsCode() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + var pub = NewSupplier("KEEP", "Public NCC", isPublic: true); + db.Suppliers.Add(pub); + await db.SaveChangesAsync(CancellationToken.None); + + await new PublishSupplierCommandHandler(db) + .Handle(new PublishSupplierCommand(pub.Id, MaNcc: null, Publish: false), CancellationToken.None); + + var reloaded = await db.Suppliers.SingleAsync(x => x.Id == pub.Id); + reloaded.IsPublic.Should().BeFalse("ẩn luôn được, KHÔNG cần Mã NCC"); + reloaded.Code.Should().Be("KEEP", "unpublish KHÔNG đụng Code"); + } + + // ---- T12 — ListSuppliers Published filter: true / false / null=all ---- + [Fact] + public async Task ListSuppliers_PublishedFilter_FiltersByIsPublic_NullReturnsAll() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + db.Suppliers.AddRange( + NewSupplier("P1", "Pub 1", isPublic: true), + NewSupplier("P2", "Pub 2", isPublic: true), + NewSupplier("", "Draft 1", isPublic: false)); + await db.SaveChangesAsync(CancellationToken.None); + + var handler = new ListSuppliersQueryHandler(db); + + var published = await handler.Handle(new ListSuppliersQuery(Published: true) { PageSize = 100 }, CancellationToken.None); + published.Total.Should().Be(2, "Published=true → chỉ NCC đã công bố"); + published.Items.Should().OnlyContain(x => x.IsPublic); + + var drafts = await handler.Handle(new ListSuppliersQuery(Published: false) { PageSize = 100 }, CancellationToken.None); + drafts.Total.Should().Be(1, "Published=false → chỉ nháp"); + drafts.Items.Should().OnlyContain(x => !x.IsPublic); + + var all = await handler.Handle(new ListSuppliersQuery(Published: null) { PageSize = 100 }, CancellationToken.None); + all.Total.Should().Be(3, "null → tất cả (admin quản lý thấy cả nháp)"); + } + + // ---- T13 — SupplierDto.IsPublic project đúng (GetSupplier single-item projection) ---- + [Fact] + public async Task GetSupplier_ProjectsIsPublic_ForBothPublicAndDraft() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + var pub = NewSupplier("G1", "Public", isPublic: true); + var draft = NewSupplier("", "Draft", isPublic: false); + db.Suppliers.AddRange(pub, draft); + await db.SaveChangesAsync(CancellationToken.None); + + var handler = new GetSupplierQueryHandler(db); + + (await handler.Handle(new GetSupplierQuery(pub.Id), CancellationToken.None)).IsPublic + .Should().BeTrue("DTO project IsPublic=true cho NCC công bố"); + (await handler.Handle(new GetSupplierQuery(draft.Id), CancellationToken.None)).IsPublic + .Should().BeFalse("DTO project IsPublic=false cho nháp"); + } +} diff --git a/tests/SolutionErp.Infrastructure.Tests/Services/SupplierImportV2DedupTests.cs b/tests/SolutionErp.Infrastructure.Tests/Services/SupplierImportV2DedupTests.cs new file mode 100644 index 0000000..b863e54 --- /dev/null +++ b/tests/SolutionErp.Infrastructure.Tests/Services/SupplierImportV2DedupTests.cs @@ -0,0 +1,452 @@ +using System.Reflection; +using System.Text.RegularExpressions; +using ClosedXML.Excel; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using SolutionErp.Application.Master.Suppliers.Dtos; +using SolutionErp.Domain.Master; +using SolutionErp.Infrastructure.Services; +using SolutionErp.Infrastructure.Tests.Common; + +namespace SolutionErp.Infrastructure.Tests.Services; + +// ============================================================================ +// Supplier import v2 (S113, owner anh Kiệt) — dedup MST-primary + Code-backstop, +// draft-on-import (IsPublic), all-or-nothing, re-baked header tokens. +// TEST-BEFORE-MERGE class: dedup = CRITICAL-ALGO (chống 500 unique-violation) + +// case-collation BUG-CLASS (S112 lesson). Test theo CODE trên đĩa (S34 single-source), +// KHÔNG sửa production — bug lộ ra → REPORT em-main. +// +// 🔑 Dedup precedence (SupplierExcelImportService.ConfirmAsync + PreviewAsync): +// existing-MST → existing-Code → batch-MST → batch-Code → New. +// - NormalizeMst = Trim + strip MỌI whitespace nội, OrdinalIgnoreCase. Blank MST → null +// (KHÔNG index) ⇒ 2 blank-MST KHÔNG coi là trùng nhau. +// - Code index OrdinalIgnoreCase, blank Code → không index. Update = FillNulls (#73-safe, +// KHÔNG đè non-null, KHÔNG đụng Code/Name/Type/IsPublic). +// +// ⚠️ SQLite fixture = BINARY collation (case-SENSITIVE). Dedup CI phải nằm ở SERVICE +// (in-memory OrdinalIgnoreCase), KHÔNG dựa DB. Test T5 (code-backstop CI) xanh = chứng +// minh service KHÔNG dựa DB collation (nếu dựa → prod SQL Server CI-unique nổ 500). +// +// ✅ FIXED S113 (NRE null-Code): NewSupplier cũ `Code = r.Code!.Trim()` NRE khi row.Code == null +// (path R4 "thiếu Mã NCC → nháp": parser sinh Code=null cho ô rỗng; preview classify New/draft). +// Fix em-main = `Code = (r.Code ?? "").Trim()` → null + "" đồng nhất → Code="" + IsPublic=false +// (draft). Test cuối `...NullCode_SavedAsDraft` chốt hành vi ĐÚNG (NotThrow + nháp). +// ============================================================================ +public class SupplierImportV2DedupTests +{ + private const int HeaderRow = 4; + + private static SupplierExcelImportService NewService(TestApplicationDbContext db) + => new(db, NullLogger.Instance); + + private static SupplierImportRowDto Row(int idx, string? code, string name, + string? taxCode = null, string? phone = null) => + new() { RowIndex = idx, Code = code, Name = name, TaxCode = taxCode, Phone = phone }; + + private static void SeedExisting(TestApplicationDbContext db, string code, string name, + string? taxCode = null, string? phone = null, bool isPublic = true) + { + db.Suppliers.Add(new Supplier + { + Id = Guid.NewGuid(), Code = code, Name = name, Type = SupplierType.NhaCungCap, + TaxCode = taxCode, Phone = phone, IsPublic = isPublic, + }); + } + + // ======================================================================== + // T1 — dedup by MST-exact: Update existing, KHÔNG insert (dù Code khác). + // ======================================================================== + [Fact] + public async Task ConfirmAsync_MstExactMatch_UpdatesExisting_NoInsert_EvenWhenCodeDiffers() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + SeedExisting(db, code: "EXA", name: "Existing A", taxCode: "0311111111", phone: null); + await db.SaveChangesAsync(CancellationToken.None); + + // Row có Code KHÁC ("ZZZ") nhưng MST TRÙNG → dedup phải bắt theo MST. + var rows = new List { Row(5, "ZZZ", "Import row", taxCode: "0311111111", phone: "0909") }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Committed.Should().BeTrue(); + result.Updated.Should().Be(1, "MST trùng → Update existing"); + result.Inserted.Should().Be(0, "KHÔNG insert bản ghi thứ 2 (dedup MST-primary)"); + (await db.Suppliers.IgnoreQueryFilters().CountAsync()).Should().Be(1); + + var only = await db.Suppliers.SingleAsync(); + only.Code.Should().Be("EXA", "FillNulls KHÔNG đụng Code — match qua MST, giữ Code gốc"); + only.Phone.Should().Be("0909", "fill-null: Phone đang null → điền (chứng minh đúng row cũ bị update)"); + } + + // ======================================================================== + // T2 — NormalizeMst: "0312 251 859" (có dấu cách) khớp "0312251859" trong DB. + // ======================================================================== + [Fact] + public async Task ConfirmAsync_MstWithInnerSpaces_NormalizedMatch_UpdatesExisting() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + SeedExisting(db, code: "EXB", name: "Existing B", taxCode: "0312251859"); + await db.SaveChangesAsync(CancellationToken.None); + + var rows = new List { Row(5, "Y", "Import", taxCode: " 0312 251 859 ") }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Updated.Should().Be(1, "MST strip-whitespace → '0312251859' khớp → Update"); + result.Inserted.Should().Be(0); + (await db.Suppliers.IgnoreQueryFilters().CountAsync()).Should().Be(1); + } + + // ======================================================================== + // T3 — 2 row đều blank-MST → 2 insert (KHÔNG merge). Blank MST không phải khóa dedup. + // ======================================================================== + [Fact] + public async Task ConfirmAsync_TwoBlankMstRows_DistinctCode_InsertsBoth_NoMerge() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + + var rows = new List + { + Row(5, "AA", "Alpha", taxCode: null), // blank MST + Row(6, "BB", "Beta", taxCode: " "), // whitespace → NormalizeMst = null (blank) + }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Inserted.Should().Be(2, "2 row blank-MST + Code khác nhau → KHÔNG gộp, insert cả 2"); + result.Updated.Should().Be(0); + result.Skipped.Should().Be(0); + (await db.Suppliers.IgnoreQueryFilters().CountAsync()).Should().Be(2, + "blank-MST KHÔNG vào batchByMst → 2 NCC không-MST KHÔNG bị coi trùng nhau"); + } + + // ======================================================================== + // T4 — 2 row CÙNG MST non-blank trong 1 batch → 1 insert + row sau fill-nulls (skip). + // ======================================================================== + [Fact] + public async Task ConfirmAsync_TwoRowsSameMstInBatch_SingleInsert_SecondFillsNulls() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + + var rows = new List + { + Row(5, "C1", "First", taxCode: "MST9", phone: null), // insert, Phone null + Row(6, "C2", "Second", taxCode: "MST9", phone: "0999"), // cùng MST → gộp vào row1 + }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Inserted.Should().Be(1, "cùng MST trong batch → chỉ 1 insert"); + result.Skipped.Should().Be(1, "row thứ 2 gộp vào bản ghi vừa thêm (fill-nulls), KHÔNG đếm Updated"); + result.Updated.Should().Be(0, "gộp trong batch (chưa persist) → không tính Updated"); + (await db.Suppliers.IgnoreQueryFilters().CountAsync()).Should().Be(1); + + var only = await db.Suppliers.SingleAsync(); + only.Code.Should().Be("C1", "giữ Code của row đầu (FillNulls không đụng Code)"); + only.Phone.Should().Be("0999", "row2 fill-null Phone vào bản ghi batch → chứng minh gộp thật"); + } + + // ======================================================================== + // T5 — no-MST + Code khớp existing (CI) → Update-by-Code backstop (chống 500). + // SQLite BINARY: "backstop" != "BACKSTOP" ở DB → dedup phải ở SERVICE (OrdinalIgnoreCase). + // ======================================================================== + [Fact] + public async Task ConfirmAsync_NoMst_CodeMatchesExistingCaseInsensitive_UpdatesByBackstop_NoInsert() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + SeedExisting(db, code: "BACKSTOP", name: "Existing", taxCode: null); + await db.SaveChangesAsync(CancellationToken.None); + + var rows = new List { Row(5, "backstop", "Import", taxCode: null) }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Updated.Should().Be(1, "Code khớp CI (service OrdinalIgnoreCase) → Update backstop"); + result.Inserted.Should().Be(0, + "KHÔNG insert 'backstop' cạnh 'BACKSTOP' — nếu dựa DB collation SQLite thì sẽ insert (prod SQL Server CI nổ 500)"); + (await db.Suppliers.IgnoreQueryFilters().CountAsync()).Should().Be(1); + } + + // ======================================================================== + // T6 — MST→A nhưng Code→B (khác entity): MST-precedence WINS. Update A, B untouched. + // KHÔNG silent-clobber: FillNulls không đụng Code nên A.Code giữ nguyên; B không bị chạm. + // ======================================================================== + [Fact] + public async Task ConfirmAsync_MstMatchesA_CodeMatchesB_MstPrecedenceWins_UpdatesA_BUntouched() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + SeedExisting(db, code: "AAA", name: "Alpha", taxCode: "MST-AAA", phone: null); + SeedExisting(db, code: "BBB", name: "Beta", taxCode: "MST-BBB", phone: null); + await db.SaveChangesAsync(CancellationToken.None); + + // Row: Code 'bbb' (CI→B) nhưng MST 'MST-AAA' (→A). Precedence MST → target A. + var rows = new List { Row(5, "bbb", "Import", taxCode: "MST-AAA", phone: "P-NEW") }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Updated.Should().Be(1, "MST-precedence → chỉ A được Update"); + result.Inserted.Should().Be(0, "không tạo entity mới"); + (await db.Suppliers.IgnoreQueryFilters().CountAsync()).Should().Be(2, "vẫn đúng 2 NCC (A + B)"); + + var a = await db.Suppliers.SingleAsync(x => x.TaxCode == "MST-AAA"); + a.Code.Should().Be("AAA", "A.Code KHÔNG bị đổi thành 'bbb' — FillNulls không đụng Code (no silent-clobber)"); + a.Phone.Should().Be("P-NEW", "A (khớp MST) nhận fill-null Phone"); + + var b = await db.Suppliers.SingleAsync(x => x.TaxCode == "MST-BBB"); + b.Code.Should().Be("BBB", "B KHÔNG bị chạm dù Code row khớp B (MST thắng)"); + b.Phone.Should().BeNull("B untouched — không nhận Phone của row"); + } + + // ======================================================================== + // T6-preview — cùng kịch bản qua PreviewAsync: classify Update + trỏ ExistingSupplierId=A. + // ======================================================================== + [Fact] + public async Task PreviewAsync_MstMatchesA_CodeMatchesB_ClassifiesUpdateTargetingA() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + var aId = Guid.NewGuid(); + db.Suppliers.Add(new Supplier { Id = aId, Code = "AAA", Name = "Alpha", Type = SupplierType.NhaCungCap, TaxCode = "MST-AAA", IsPublic = true }); + db.Suppliers.Add(new Supplier { Id = Guid.NewGuid(), Code = "BBB", Name = "Beta", Type = SupplierType.NhaCungCap, TaxCode = "MST-BBB", IsPublic = true }); + await db.SaveChangesAsync(CancellationToken.None); + + using var stream = BuildWorkbook(ReflectExpectedHeaderTokens(), ws => + { + ws.Cell(5, 4).Value = "bbb"; // Code → B + ws.Cell(5, 5).Value = "Import"; + ws.Cell(5, 12).Value = "MST-AAA"; // MST → A + }); + + var preview = await NewService(db).PreviewAsync(stream, CancellationToken.None); + + preview.LayoutValid.Should().BeTrue(); + var row = preview.Rows.Single(); + row.Status.Should().Be(RowImportStatus.Update, "MST-precedence → Update"); + row.ExistingSupplierId.Should().Be(aId, "trỏ tới A (khớp MST), KHÔNG phải B (khớp Code)"); + } + + // ======================================================================== + // T7 — MST→A ∧ Code→A (cùng entity): 1 Update, nhất quán, no conflict. + // ======================================================================== + [Fact] + public async Task ConfirmAsync_MstAndCodeBothMatchSameEntity_SingleUpdate_NoInsert() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + SeedExisting(db, code: "SAME", name: "Existing", taxCode: "MST-S", phone: null); + await db.SaveChangesAsync(CancellationToken.None); + + var rows = new List { Row(5, "same", "Import", taxCode: "MST-S", phone: "P7") }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Updated.Should().Be(1); + result.Inserted.Should().Be(0); + (await db.Suppliers.IgnoreQueryFilters().CountAsync()).Should().Be(1); + (await db.Suppliers.SingleAsync()).Phone.Should().Be("P7"); + } + + // ======================================================================== + // T8 — draft-on-import: blank Code → IsPublic=false (nháp) + DraftCount; có Code → IsPublic=true. + // Blank Code dùng "" (empty) — path được service HỖ TRỢ (null NREs, xem test bug bên dưới). + // ======================================================================== + [Fact] + public async Task ConfirmAsync_BlankCodeRow_IsDraft_CodedRow_IsPublic() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + + var rows = new List + { + Row(5, "", "Draft NCC"), // blank Code → nháp + Row(6, "PUB1", "Public NCC"), // có Code → publish-able + }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Committed.Should().BeTrue(); + result.Inserted.Should().Be(2); + result.DraftCount.Should().Be(1, "chỉ row blank-Code tính là nháp"); + + var draft = await db.Suppliers.SingleAsync(x => x.Name == "Draft NCC"); + draft.IsPublic.Should().BeFalse("thiếu Mã NCC → IsPublic=false (R4)"); + var pub = await db.Suppliers.SingleAsync(x => x.Name == "Public NCC"); + pub.IsPublic.Should().BeTrue("có Mã NCC → IsPublic=true"); + } + + // ======================================================================== + // T14 — blank Code KHÔNG còn là hard-error (v2 đổi S112): batch commit, row thành nháp. + // ======================================================================== + [Fact] + public async Task ConfirmAsync_BlankCode_IsNotHardError_CommitsAsDraft() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + + var rows = new List + { + Row(5, "", "Only-name NCC"), // KHÔNG Code, CÓ Name → hợp lệ (draft) + }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Committed.Should().BeTrue("blank Code KHÔNG abort batch (v2)"); + result.Errors.Should().BeEmpty(); + result.Inserted.Should().Be(1); + (await db.Suppliers.SingleAsync()).IsPublic.Should().BeFalse(); + } + + // ======================================================================== + // T14b — blank Name VẪN là hard-error → all-or-nothing abort (kể cả draft row hợp lệ cùng batch). + // ======================================================================== + [Fact] + public async Task ConfirmAsync_BlankName_StillHardError_AbortsWholeBatch_IncludingValidDraftRow() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + + var rows = new List + { + Row(5, "", "Valid draft"), // hợp lệ (blank Code OK) + Row(6, "HASCODE", " "), // hard-error: thiếu Name + }; + var result = await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + result.Committed.Should().BeFalse("1 hard-error (thiếu Name) → all-or-nothing abort"); + result.Errors.Should().NotBeEmpty(); + result.Inserted.Should().Be(0); + (await db.Suppliers.IgnoreQueryFilters().CountAsync()) + .Should().Be(0, "kể cả 'Valid draft' cũng KHÔNG ghi (all-or-nothing)"); + } + + // ======================================================================== + // T15 — ExpectedHeaderTokens re-baked = ĐÚNG 30 token; header dựng từ chính tokens (reflection) + // → LayoutValid=true. Real-file tokens (dạng \n→space) cũng LayoutValid=true (normalize collapse). + // ======================================================================== + [Fact] + public async Task PreviewAsync_ReBakedExpectedTokens_ThirtyTokens_LayoutValidTrue_And_RealTokensAlsoValid() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + + var expected = ReflectExpectedHeaderTokens(); + expected.Should().HaveCount(30, "re-bake phải đúng 30 token row-4"); + + // (a) header = chính ExpectedHeaderTokens (service phải chấp nhận file có header khớp hệt). + using var streamExpected = BuildWorkbook(expected, ws => + { + ws.Cell(5, 4).Value = "RT-01"; + ws.Cell(5, 5).Value = "NCC token thật"; + }); + var pExpected = await NewService(db).PreviewAsync(streamExpected, CancellationToken.None); + pExpected.LayoutValid.Should().BeTrue("header = ExpectedHeaderTokens → LayoutValid=true"); + pExpected.Rows.Should().ContainSingle(); + + // (b) faithful real-file tokens (\n thay bằng space) — NormalizeHeader collapse \n → phải khớp. + var real = RealFileHeaderTokensSingleSpace(); + NormalizeJoinFaithful(real).Should().Be(NormalizeJoinFaithful(expected), + "sau NormalizeHeader (collapse \\n/space + FormC), real == expected → re-bake khớp file thật"); + + using var streamReal = BuildWorkbook(real, ws => + { + ws.Cell(5, 4).Value = "RT-02"; + ws.Cell(5, 5).Value = "NCC real"; + }); + var pReal = await NewService(db).PreviewAsync(streamReal, CancellationToken.None); + pReal.LayoutValid.Should().BeTrue("real tokens (space form) normalize khớp → LayoutValid=true"); + } + + // ======================================================================== + // ✅ FIXED S113 — new-row Code=null → lưu NHÁP, KHÔNG NRE. + // Path R4 "thiếu Mã NCC → nháp": parser sinh null cho ô cột-4 rỗng; preview classify New/draft. + // NewSupplier cũ `Code = r.Code!.Trim()` → NRE-abort-batch (500). Fix: `(r.Code ?? "").Trim()`. + // null + "" giờ đồng nhất → Code="" + IsPublic=false (draft, ẩn khỏi filtered-unique [Code]<>''). + // ======================================================================== + [Fact] + public async Task ConfirmAsync_NewRow_NullCode_SavedAsDraft() + { + using var fix = new SqliteDbFixture(); + var db = fix.Db; + + var rows = new List { Row(5, null, "NCC thiếu Mã NCC") }; + var act = async () => await NewService(db).ConfirmAsync(rows, "tester", CancellationToken.None); + + await act.Should().NotThrowAsync("S113 fix: Code=null → \"\" → nháp, KHÔNG còn NRE"); + var saved = await db.Suppliers.IgnoreQueryFilters().SingleAsync(); + saved.Code.Should().BeEmpty("null Code → \"\" sau trim"); + saved.IsPublic.Should().BeFalse("thiếu Mã NCC → nháp/ẩn (R4)"); + } + + // ======================================================================== + // Helpers + // ======================================================================== + private static string[] ReflectExpectedHeaderTokens() + { + var f = typeof(SupplierExcelImportService) + .GetField("ExpectedHeaderTokens", BindingFlags.NonPublic | BindingFlags.Static); + f.Should().NotBeNull("service phải có static ExpectedHeaderTokens (đọc qua reflection)"); + return (string[])f!.GetValue(null)!; + } + + private static MemoryStream BuildWorkbook(string[] headerRow4, Action writeData) + { + var ms = new MemoryStream(); + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + for (int c = 1; c <= headerRow4.Length; c++) + ws.Cell(HeaderRow, c).Value = headerRow4[c - 1]; + writeData(ws); + wb.SaveAs(ms); + } + ms.Position = 0; + return ms; + } + + // Faithful replicate SupplierExcelImportService.NormalizeHeader (INCLUDING .Normalize(FormC), + // khác test S112 cũ thiếu FormC) → so-khớp deterministic, không mis-predict. + private static string NormalizeJoinFaithful(IEnumerable tokens) + { + static string Norm(string raw) + { + if (string.IsNullOrEmpty(raw)) return string.Empty; + var collapsed = Regex.Replace(raw.Replace("\n", " ").Replace("\r", " "), @"\s+", " "); + return collapsed.Trim().ToUpperInvariant().Normalize(System.Text.NormalizationForm.FormC); + } + return string.Join(" | ", tokens.Select(Norm)); + } + + // 30 token row-4 file "Database NCC" thật — dạng single-space (\n gốc thay bằng space). + // NormalizeHeader collapse \n→space nên phải khớp ExpectedHeaderTokens (dạng \n) sau normalize. + private static string[] RealFileHeaderTokensSingleSpace() => + [ + "STT", + "GÓI THẦU", + "PHÂN LOẠI (NTP/NCC/Cả hai)", + "TÊN VIẾT TẮT (Dùng trong HĐ)", + "TÊN CÔNG TY (Đầy đủ, đúng pháp lý)", + "ĐỊA CHỈ XUẤT HÓA ĐƠN (Địa chỉ đăng ký kinh doanh)", + "ĐỊA CHỈ VĂN PHÒNG (nếu có)", + "SỐ ĐIỆN THOẠI CÔNG TY", + "FAX", + "SỐ TÀI KHOẢN+ TÊN+CN. NGÂN HÀNG (Đầy đủ, đúng pháp lý)", + "SỐ TK PHỤ (nếu có)", + "MÃ SỐ THUẾ", + "NGƯỜI ĐẠI DIỆN PHÁP LUẬT", + "CHỨC VỤ ĐẠI DIỆN", + "GIẤY ỦY QUYỀN (số, ngày, người ủy quyền)", + "Link GUQ", + "Link GPKD", + "Link HSNL", + "NGƯỜI LIÊN HỆ CHÍNH", + "CHỨC VỤ NGƯỜI LH", + "SĐT CHÍNH", + "EMAIL", + "ĐỊA CHỈ GỬI THƯ", + "NGƯỜI NHẬN THƯ/ SDT", + "NGUỒN GIỚI THIỆU", + "NGƯỜI PHỤ TRÁCH (PMH)", + "TÌNH TRẠNG HIỆN TẠI", + "GHI CHÚ / LÝ DO BLACKLIST", + "NGÀY CẬP NHẬT CUỐI", + "NGƯỜI CẬP NHẬT", + ]; +}
{r.code || '—'} + updateRow(r.rowIndex, { code: e.target.value })} + placeholder="Mã NCC" + className="h-7 font-mono text-xs" + /> + {r.name || '—'} {SupplierTypeLabel[r.type] ?? '—'} - {r.messages.length > 0 ? r.messages.join('; ') : ''} + {r.mstMissing &&
⚠️ Chưa có MST, có thể bị trùng
} + {r.messages.length > 0 &&
{r.messages.join('; ')}
}