[CLAUDE] PE: section Bang so sanh + rename demo email @solutions.com.vn
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m10s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m10s
PART A: Section 'Bang so sanh' (file tong ho so so sanh) User request: 'theo them cho thong tin ve Bang so sanh, cho dinh kem file so sanh tong len'. BE: - PurchaseEvaluationAttachmentPurpose.ComparisonTable = 4 (new enum value) Backend validator IsInEnum pass, khong can migration (int column). FE types (2 app): - PeAttachmentPurpose.ComparisonTable + Label '4: Bang so sanh'. FE PeDetailTabs: - Them section thu 4 'Bang so sanh (file tong)' sau 'Hang muc + Bao gia'. - Component GeneralAttachmentsSection: upload KHONG truyen supplierRowId (BE luu NULL) → purpose=ComparisonTable default. Filter attachments co supplierRowId===null de render. - Card layout khac SupplierAttachmentsCell: full-width card + brand color + purpose chip + date. Upload button to hon ([+ Tai len bang so sanh]). - readOnly hide upload + delete, giu download. PART B: Demo email rebrand @solutionerp.local → @solutions.com.vn User request: 'tao nguoi dung demo theo email cua ben nay'. BE DbInitializer: - Rename 18 email in source: AdminEmail const + 17 demo users (bod/pm/ccm/pro/fin/act/equ/hra/qs/nv) — keep password + role unchanged. - Them BackfillUserEmailDomainAsync (idempotent): scan user co email @solutionerp.local, rename sang @solutions.com.vn, update Email + NormalizedEmail + UserName + NormalizedUserName. Skip neu co conflict user da ton tai voi email moi. Chay truoc SeedAdmin de tranh tao duplicate admin. Admin permission tao user da co san qua /system/users page. Comment input khi duyet da co san o PeWorkflowPanel (Ghi chu tuy chon Textarea) + ContractDetailContent (Yeu cau sua / Duyet tiep dialog).
This commit is contained in:
@ -104,6 +104,13 @@ export function PeDetailTabs({
|
|||||||
<Section title={`Hạng mục + Báo giá (${evaluation.details.length})`}>
|
<Section title={`Hạng mục + Báo giá (${evaluation.details.length})`}>
|
||||||
<ItemsTab ev={evaluation} readOnly={readOnly} />
|
<ItemsTab ev={evaluation} readOnly={readOnly} />
|
||||||
</Section>
|
</Section>
|
||||||
|
<Section title="Bảng so sánh (file tổng)">
|
||||||
|
<GeneralAttachmentsSection
|
||||||
|
evaluationId={evaluation.id}
|
||||||
|
attachments={evaluation.attachments.filter(a => a.purchaseEvaluationSupplierId === null)}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -869,3 +876,140 @@ function SupplierAttachmentsCell({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Section Bảng so sánh — general attachments (không gắn NCC cụ thể) =====
|
||||||
|
// Purpose mặc định = ComparisonTable (4). Upload file Excel/PDF tổng hợp so
|
||||||
|
// sánh giá N NCC × M hạng mục. Storage path giống SupplierAttachmentsCell
|
||||||
|
// nhưng supplierRowId KHÔNG truyền → BE lưu NULL.
|
||||||
|
function GeneralAttachmentsSection({
|
||||||
|
evaluationId,
|
||||||
|
attachments,
|
||||||
|
readOnly = false,
|
||||||
|
}: {
|
||||||
|
evaluationId: string
|
||||||
|
attachments: PeAttachment[]
|
||||||
|
readOnly?: boolean
|
||||||
|
}) {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const upload = useMutation({
|
||||||
|
mutationFn: async (file: File) => {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', file)
|
||||||
|
// KHÔNG append supplierRowId → BE set NULL → general attachment
|
||||||
|
fd.append('purpose', String(PeAttachmentPurpose.ComparisonTable))
|
||||||
|
return api.post(`/purchase-evaluations/${evaluationId}/attachments`, fd, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Đã tải lên bảng so sánh.')
|
||||||
|
qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
|
||||||
|
},
|
||||||
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const del = useMutation({
|
||||||
|
mutationFn: async (attId: string) =>
|
||||||
|
api.delete(`/purchase-evaluations/${evaluationId}/attachments/${attId}`),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Đã xóa.')
|
||||||
|
qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
|
||||||
|
},
|
||||||
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
|
})
|
||||||
|
|
||||||
|
async function download(att: PeAttachment) {
|
||||||
|
try {
|
||||||
|
const res = await api.get(
|
||||||
|
`/purchase-evaluations/${evaluationId}/attachments/${att.id}/download`,
|
||||||
|
{ responseType: 'blob' },
|
||||||
|
)
|
||||||
|
const url = window.URL.createObjectURL(res.data as Blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = att.fileName
|
||||||
|
a.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(getErrorMessage(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPick(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const f = e.target.files?.[0]
|
||||||
|
if (f) upload.mutate(f)
|
||||||
|
e.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmtSize = (b: number) =>
|
||||||
|
b > 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)}MB` : `${Math.round(b / 1024)}KB`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!readOnly && (
|
||||||
|
<p className="mb-2 text-[12px] text-slate-500">
|
||||||
|
File Excel/PDF tổng hợp so sánh giá của tất cả NCC (không gắn với 1 NCC cụ thể).
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{attachments.length === 0 && readOnly && (
|
||||||
|
<p className="text-sm italic text-slate-400">Chưa có bảng so sánh.</p>
|
||||||
|
)}
|
||||||
|
{attachments.length > 0 && (
|
||||||
|
<div className="mb-2 space-y-1.5">
|
||||||
|
{attachments.map(a => (
|
||||||
|
<div
|
||||||
|
key={a.id}
|
||||||
|
className="flex items-center gap-2 rounded border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm"
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4 shrink-0 text-brand-500" />
|
||||||
|
<button
|
||||||
|
onClick={() => download(a)}
|
||||||
|
className="min-w-0 flex-1 truncate text-left font-medium text-slate-800 hover:text-brand-700 hover:underline"
|
||||||
|
title={a.fileName}
|
||||||
|
>
|
||||||
|
{a.fileName}
|
||||||
|
</button>
|
||||||
|
<span className="shrink-0 text-[11px] text-slate-500">{fmtSize(a.fileSize)}</span>
|
||||||
|
<span className="shrink-0 rounded bg-brand-50 px-1.5 py-0.5 text-[10px] text-brand-700">
|
||||||
|
{PeAttachmentPurposeLabel[a.purpose] ?? 'Khác'}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-[10px] text-slate-400">
|
||||||
|
{new Date(a.createdAt).toLocaleDateString('vi-VN')}
|
||||||
|
</span>
|
||||||
|
{!readOnly && (
|
||||||
|
<button
|
||||||
|
onClick={() => { if (confirm(`Xóa "${a.fileName}"?`)) del.mutate(a.id) }}
|
||||||
|
className="shrink-0 rounded p-1 text-red-500 hover:bg-red-50"
|
||||||
|
title="Xóa"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!readOnly && (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp"
|
||||||
|
onChange={onPick}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={upload.isPending}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded border border-dashed border-brand-300 bg-brand-50/50 px-3 py-2 text-xs font-medium text-brand-700 hover:border-brand-500 hover:bg-brand-50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Upload className="h-3.5 w-3.5" />
|
||||||
|
{upload.isPending ? 'Đang tải…' : '+ Tải lên bảng so sánh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -121,6 +121,7 @@ export const PeAttachmentPurpose = {
|
|||||||
QuoteDocument: 1,
|
QuoteDocument: 1,
|
||||||
RequirementSpec: 2,
|
RequirementSpec: 2,
|
||||||
DecisionExport: 3,
|
DecisionExport: 3,
|
||||||
|
ComparisonTable: 4,
|
||||||
Other: 99,
|
Other: 99,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@ -128,6 +129,7 @@ export const PeAttachmentPurposeLabel: Record<number, string> = {
|
|||||||
1: 'Báo giá',
|
1: 'Báo giá',
|
||||||
2: 'Yêu cầu KT',
|
2: 'Yêu cầu KT',
|
||||||
3: 'Phiếu duyệt',
|
3: 'Phiếu duyệt',
|
||||||
|
4: 'Bảng so sánh',
|
||||||
99: 'Khác',
|
99: 'Khác',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -104,6 +104,13 @@ export function PeDetailTabs({
|
|||||||
<Section title={`Hạng mục + Báo giá (${evaluation.details.length})`}>
|
<Section title={`Hạng mục + Báo giá (${evaluation.details.length})`}>
|
||||||
<ItemsTab ev={evaluation} readOnly={readOnly} />
|
<ItemsTab ev={evaluation} readOnly={readOnly} />
|
||||||
</Section>
|
</Section>
|
||||||
|
<Section title="Bảng so sánh (file tổng)">
|
||||||
|
<GeneralAttachmentsSection
|
||||||
|
evaluationId={evaluation.id}
|
||||||
|
attachments={evaluation.attachments.filter(a => a.purchaseEvaluationSupplierId === null)}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -869,3 +876,140 @@ function SupplierAttachmentsCell({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Section Bảng so sánh — general attachments (không gắn NCC cụ thể) =====
|
||||||
|
// Purpose mặc định = ComparisonTable (4). Upload file Excel/PDF tổng hợp so
|
||||||
|
// sánh giá N NCC × M hạng mục. Storage path giống SupplierAttachmentsCell
|
||||||
|
// nhưng supplierRowId KHÔNG truyền → BE lưu NULL.
|
||||||
|
function GeneralAttachmentsSection({
|
||||||
|
evaluationId,
|
||||||
|
attachments,
|
||||||
|
readOnly = false,
|
||||||
|
}: {
|
||||||
|
evaluationId: string
|
||||||
|
attachments: PeAttachment[]
|
||||||
|
readOnly?: boolean
|
||||||
|
}) {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const upload = useMutation({
|
||||||
|
mutationFn: async (file: File) => {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', file)
|
||||||
|
// KHÔNG append supplierRowId → BE set NULL → general attachment
|
||||||
|
fd.append('purpose', String(PeAttachmentPurpose.ComparisonTable))
|
||||||
|
return api.post(`/purchase-evaluations/${evaluationId}/attachments`, fd, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Đã tải lên bảng so sánh.')
|
||||||
|
qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
|
||||||
|
},
|
||||||
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const del = useMutation({
|
||||||
|
mutationFn: async (attId: string) =>
|
||||||
|
api.delete(`/purchase-evaluations/${evaluationId}/attachments/${attId}`),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Đã xóa.')
|
||||||
|
qc.invalidateQueries({ queryKey: ['pe-detail', evaluationId] })
|
||||||
|
},
|
||||||
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
|
})
|
||||||
|
|
||||||
|
async function download(att: PeAttachment) {
|
||||||
|
try {
|
||||||
|
const res = await api.get(
|
||||||
|
`/purchase-evaluations/${evaluationId}/attachments/${att.id}/download`,
|
||||||
|
{ responseType: 'blob' },
|
||||||
|
)
|
||||||
|
const url = window.URL.createObjectURL(res.data as Blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = att.fileName
|
||||||
|
a.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(getErrorMessage(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPick(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const f = e.target.files?.[0]
|
||||||
|
if (f) upload.mutate(f)
|
||||||
|
e.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmtSize = (b: number) =>
|
||||||
|
b > 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)}MB` : `${Math.round(b / 1024)}KB`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!readOnly && (
|
||||||
|
<p className="mb-2 text-[12px] text-slate-500">
|
||||||
|
File Excel/PDF tổng hợp so sánh giá của tất cả NCC (không gắn với 1 NCC cụ thể).
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{attachments.length === 0 && readOnly && (
|
||||||
|
<p className="text-sm italic text-slate-400">Chưa có bảng so sánh.</p>
|
||||||
|
)}
|
||||||
|
{attachments.length > 0 && (
|
||||||
|
<div className="mb-2 space-y-1.5">
|
||||||
|
{attachments.map(a => (
|
||||||
|
<div
|
||||||
|
key={a.id}
|
||||||
|
className="flex items-center gap-2 rounded border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm"
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4 shrink-0 text-brand-500" />
|
||||||
|
<button
|
||||||
|
onClick={() => download(a)}
|
||||||
|
className="min-w-0 flex-1 truncate text-left font-medium text-slate-800 hover:text-brand-700 hover:underline"
|
||||||
|
title={a.fileName}
|
||||||
|
>
|
||||||
|
{a.fileName}
|
||||||
|
</button>
|
||||||
|
<span className="shrink-0 text-[11px] text-slate-500">{fmtSize(a.fileSize)}</span>
|
||||||
|
<span className="shrink-0 rounded bg-brand-50 px-1.5 py-0.5 text-[10px] text-brand-700">
|
||||||
|
{PeAttachmentPurposeLabel[a.purpose] ?? 'Khác'}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-[10px] text-slate-400">
|
||||||
|
{new Date(a.createdAt).toLocaleDateString('vi-VN')}
|
||||||
|
</span>
|
||||||
|
{!readOnly && (
|
||||||
|
<button
|
||||||
|
onClick={() => { if (confirm(`Xóa "${a.fileName}"?`)) del.mutate(a.id) }}
|
||||||
|
className="shrink-0 rounded p-1 text-red-500 hover:bg-red-50"
|
||||||
|
title="Xóa"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!readOnly && (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp"
|
||||||
|
onChange={onPick}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
disabled={upload.isPending}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded border border-dashed border-brand-300 bg-brand-50/50 px-3 py-2 text-xs font-medium text-brand-700 hover:border-brand-500 hover:bg-brand-50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<Upload className="h-3.5 w-3.5" />
|
||||||
|
{upload.isPending ? 'Đang tải…' : '+ Tải lên bảng so sánh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -121,6 +121,7 @@ export const PeAttachmentPurpose = {
|
|||||||
QuoteDocument: 1,
|
QuoteDocument: 1,
|
||||||
RequirementSpec: 2,
|
RequirementSpec: 2,
|
||||||
DecisionExport: 3,
|
DecisionExport: 3,
|
||||||
|
ComparisonTable: 4,
|
||||||
Other: 99,
|
Other: 99,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@ -128,6 +129,7 @@ export const PeAttachmentPurposeLabel: Record<number, string> = {
|
|||||||
1: 'Báo giá',
|
1: 'Báo giá',
|
||||||
2: 'Yêu cầu KT',
|
2: 'Yêu cầu KT',
|
||||||
3: 'Phiếu duyệt',
|
3: 'Phiếu duyệt',
|
||||||
|
4: 'Bảng so sánh',
|
||||||
99: 'Khác',
|
99: 'Khác',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,10 @@ namespace SolutionErp.Domain.PurchaseEvaluations;
|
|||||||
|
|
||||||
public enum PurchaseEvaluationAttachmentPurpose
|
public enum PurchaseEvaluationAttachmentPurpose
|
||||||
{
|
{
|
||||||
QuoteDocument = 1, // File báo giá NCC gửi (PDF/xlsx)
|
QuoteDocument = 1, // File báo giá NCC gửi (PDF/xlsx) — gắn với NCC cụ thể
|
||||||
RequirementSpec = 2, // Bản vẽ/yêu cầu kỹ thuật kèm theo
|
RequirementSpec = 2, // Bản vẽ/yêu cầu kỹ thuật kèm theo
|
||||||
DecisionExport = 3, // Bản phiếu duyệt đã export
|
DecisionExport = 3, // Bản phiếu duyệt đã export
|
||||||
|
ComparisonTable = 4, // Bảng so sánh tổng — không gắn với 1 NCC, file tổng hợp
|
||||||
Other = 99,
|
Other = 99,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,9 +14,39 @@ namespace SolutionErp.Infrastructure.Persistence;
|
|||||||
|
|
||||||
public static class DbInitializer
|
public static class DbInitializer
|
||||||
{
|
{
|
||||||
public const string AdminEmail = "admin@solutionerp.local";
|
public const string AdminEmail = "admin@solutions.com.vn";
|
||||||
public const string AdminPassword = "Admin@123456";
|
public const string AdminPassword = "Admin@123456";
|
||||||
|
|
||||||
|
// Migration helper (Phase 6 rebrand): rename user email
|
||||||
|
// @solutionerp.local → @solutions.com.vn. Idempotent — skip user đã rename.
|
||||||
|
// Password + role + refresh token giữ nguyên. Chạy trước SeedAdmin.
|
||||||
|
private static async Task BackfillUserEmailDomainAsync(
|
||||||
|
UserManager<User> userManager, ILogger logger)
|
||||||
|
{
|
||||||
|
const string OldDomain = "@solutionerp.local";
|
||||||
|
const string NewDomain = "@solutions.com.vn";
|
||||||
|
var oldUsers = userManager.Users
|
||||||
|
.Where(u => u.Email != null && u.Email.EndsWith(OldDomain))
|
||||||
|
.ToList();
|
||||||
|
if (oldUsers.Count == 0) return;
|
||||||
|
|
||||||
|
var renamed = 0;
|
||||||
|
foreach (var u in oldUsers)
|
||||||
|
{
|
||||||
|
var newEmail = u.Email!.Replace(OldDomain, NewDomain);
|
||||||
|
var conflict = await userManager.FindByEmailAsync(newEmail);
|
||||||
|
if (conflict != null && conflict.Id != u.Id) continue;
|
||||||
|
u.Email = newEmail;
|
||||||
|
u.NormalizedEmail = newEmail.ToUpperInvariant();
|
||||||
|
u.UserName = newEmail;
|
||||||
|
u.NormalizedUserName = newEmail.ToUpperInvariant();
|
||||||
|
var res = await userManager.UpdateAsync(u);
|
||||||
|
if (res.Succeeded) renamed++;
|
||||||
|
}
|
||||||
|
if (renamed > 0)
|
||||||
|
logger.LogInformation("Backfilled {Count} user emails -> @solutions.com.vn", renamed);
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task InitializeAsync(IServiceProvider services)
|
public static async Task InitializeAsync(IServiceProvider services)
|
||||||
{
|
{
|
||||||
using var scope = services.CreateScope();
|
using var scope = services.CreateScope();
|
||||||
@ -30,6 +60,9 @@ public static class DbInitializer
|
|||||||
await db.Database.MigrateAsync();
|
await db.Database.MigrateAsync();
|
||||||
|
|
||||||
await SeedRolesAsync(roleManager, logger);
|
await SeedRolesAsync(roleManager, logger);
|
||||||
|
// Phase 6 rebrand: rename user email @solutionerp.local → @solutions.com.vn
|
||||||
|
// TRƯỚC SeedAdmin để SeedAdmin tìm theo new email thấy user đã rename → skip create.
|
||||||
|
await BackfillUserEmailDomainAsync(userManager, logger);
|
||||||
await SeedAdminAsync(userManager, logger);
|
await SeedAdminAsync(userManager, logger);
|
||||||
await SeedDepartmentsAsync(db, logger);
|
await SeedDepartmentsAsync(db, logger);
|
||||||
await SeedDemoUsersAsync(db, userManager, logger);
|
await SeedDemoUsersAsync(db, userManager, logger);
|
||||||
@ -452,10 +485,10 @@ public static class DbInitializer
|
|||||||
var fallbackSupplier = suppliersByCode.Values.First();
|
var fallbackSupplier = suppliersByCode.Values.First();
|
||||||
var fallbackProject = projectsByCode.Values.First();
|
var fallbackProject = projectsByCode.Values.First();
|
||||||
|
|
||||||
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutionerp.local");
|
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutions.com.vn");
|
||||||
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutionerp.local");
|
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutions.com.vn");
|
||||||
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutionerp.local");
|
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutions.com.vn");
|
||||||
var hraDang = await userManager.FindByEmailAsync("hra.dang@solutionerp.local");
|
var hraDang = await userManager.FindByEmailAsync("hra.dang@solutions.com.vn");
|
||||||
var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id;
|
var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id;
|
||||||
var nowUtc = DateTime.UtcNow;
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -704,11 +737,11 @@ public static class DbInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lookup actor users (per role)
|
// Lookup actor users (per role)
|
||||||
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutionerp.local");
|
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutions.com.vn");
|
||||||
var proPham = await userManager.FindByEmailAsync("pro.pham@solutionerp.local");
|
var proPham = await userManager.FindByEmailAsync("pro.pham@solutions.com.vn");
|
||||||
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutionerp.local");
|
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutions.com.vn");
|
||||||
var pmNguyen = await userManager.FindByEmailAsync("pm.nguyen@solutionerp.local");
|
var pmNguyen = await userManager.FindByEmailAsync("pm.nguyen@solutions.com.vn");
|
||||||
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutionerp.local");
|
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutions.com.vn");
|
||||||
var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id;
|
var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id;
|
||||||
var nowUtc = DateTime.UtcNow;
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -1101,28 +1134,28 @@ public static class DbInitializer
|
|||||||
{
|
{
|
||||||
// (Email, FullName, DeptCode, Position, RoleNames[])
|
// (Email, FullName, DeptCode, Position, RoleNames[])
|
||||||
// BOD (3) — director + signer + admin assistant
|
// BOD (3) — director + signer + admin assistant
|
||||||
("bod.huynh@solutionerp.local", "Huỳnh Văn Hùng", "BOD", "Tổng Giám đốc", new[] { AppRoles.Director }),
|
("bod.huynh@solutions.com.vn", "Huỳnh Văn Hùng", "BOD", "Tổng Giám đốc", new[] { AppRoles.Director }),
|
||||||
("bod.le@solutionerp.local", "Lê Thị Mai", "BOD", "Phó Giám đốc (NĐUQ)", new[] { AppRoles.AuthorizedSigner }),
|
("bod.le@solutions.com.vn", "Lê Thị Mai", "BOD", "Phó Giám đốc (NĐUQ)", new[] { AppRoles.AuthorizedSigner }),
|
||||||
("bod.tran@solutionerp.local", "Trần Quốc Bảo", "BOD", "Phó Giám đốc Kỹ thuật (NĐUQ)", new[] { AppRoles.AuthorizedSigner }),
|
("bod.tran@solutions.com.vn", "Trần Quốc Bảo", "BOD", "Phó Giám đốc Kỹ thuật (NĐUQ)", new[] { AppRoles.AuthorizedSigner }),
|
||||||
|
|
||||||
// PM (2) — giám đốc dự án
|
// PM (2) — giám đốc dự án
|
||||||
("pm.nguyen@solutionerp.local", "Nguyễn Quốc Cường", "PM", "Giám đốc Dự án FLOCK 01", new[] { AppRoles.ProjectManager }),
|
("pm.nguyen@solutions.com.vn", "Nguyễn Quốc Cường", "PM", "Giám đốc Dự án FLOCK 01", new[] { AppRoles.ProjectManager }),
|
||||||
("pm.le@solutionerp.local", "Lê Thanh Tùng", "PM", "Giám đốc Dự án Vinhomes Ocean Park", new[] { AppRoles.ProjectManager }),
|
("pm.le@solutions.com.vn", "Lê Thanh Tùng", "PM", "Giám đốc Dự án Vinhomes Ocean Park", new[] { AppRoles.ProjectManager }),
|
||||||
|
|
||||||
// CCM/PRO/FIN/ACT/EQU/HRA — Trưởng phòng các phòng ban (1 mỗi)
|
// CCM/PRO/FIN/ACT/EQU/HRA — Trưởng phòng các phòng ban (1 mỗi)
|
||||||
("ccm.tran@solutionerp.local", "Trần Văn Bình", "CCM", "Trưởng phòng Kiểm soát chi phí", new[] { AppRoles.CostControl, AppRoles.DeptManager }),
|
("ccm.tran@solutions.com.vn", "Trần Văn Bình", "CCM", "Trưởng phòng Kiểm soát chi phí", new[] { AppRoles.CostControl, AppRoles.DeptManager }),
|
||||||
("pro.pham@solutionerp.local", "Phạm Thị Hồng", "PRO", "Trưởng phòng Cung ứng", new[] { AppRoles.Procurement, AppRoles.DeptManager }),
|
("pro.pham@solutions.com.vn", "Phạm Thị Hồng", "PRO", "Trưởng phòng Cung ứng", new[] { AppRoles.Procurement, AppRoles.DeptManager }),
|
||||||
("fin.do@solutionerp.local", "Đỗ Minh Tuấn", "FIN", "Trưởng phòng Tài chính", new[] { AppRoles.Finance, AppRoles.DeptManager }),
|
("fin.do@solutions.com.vn", "Đỗ Minh Tuấn", "FIN", "Trưởng phòng Tài chính", new[] { AppRoles.Finance, AppRoles.DeptManager }),
|
||||||
("act.vu@solutionerp.local", "Vũ Thị Lan", "ACT", "Kế toán trưởng", new[] { AppRoles.Accounting, AppRoles.DeptManager }),
|
("act.vu@solutions.com.vn", "Vũ Thị Lan", "ACT", "Kế toán trưởng", new[] { AppRoles.Accounting, AppRoles.DeptManager }),
|
||||||
("equ.bui@solutionerp.local", "Bùi Văn Khánh", "EQU", "Trưởng phòng Thiết bị", new[] { AppRoles.Equipment, AppRoles.DeptManager }),
|
("equ.bui@solutions.com.vn", "Bùi Văn Khánh", "EQU", "Trưởng phòng Thiết bị", new[] { AppRoles.Equipment, AppRoles.DeptManager }),
|
||||||
("hra.dang@solutionerp.local", "Đặng Thị Thanh", "HRA", "Trưởng phòng Nhân sự - Hành chính", new[] { AppRoles.HrAdmin, AppRoles.DeptManager }),
|
("hra.dang@solutions.com.vn", "Đặng Thị Thanh", "HRA", "Trưởng phòng Nhân sự - Hành chính", new[] { AppRoles.HrAdmin, AppRoles.DeptManager }),
|
||||||
|
|
||||||
// Drafter (5) — soạn thảo HĐ ở các phòng khác nhau
|
// Drafter (5) — soạn thảo HĐ ở các phòng khác nhau
|
||||||
("qs.hoang@solutionerp.local", "Hoàng Văn Đức", "QS", "QS công trường — soạn thảo HĐ", new[] { AppRoles.Drafter }),
|
("qs.hoang@solutions.com.vn", "Hoàng Văn Đức", "QS", "QS công trường — soạn thảo HĐ", new[] { AppRoles.Drafter }),
|
||||||
("qs.ngo@solutionerp.local", "Ngô Thị Hà", "QS", "QS dự án FLOCK 01", new[] { AppRoles.Drafter }),
|
("qs.ngo@solutions.com.vn", "Ngô Thị Hà", "QS", "QS dự án FLOCK 01", new[] { AppRoles.Drafter }),
|
||||||
("nv.cao@solutionerp.local", "Cao Văn Long", "PRO", "Nhân viên Cung ứng", new[] { AppRoles.Drafter }),
|
("nv.cao@solutions.com.vn", "Cao Văn Long", "PRO", "Nhân viên Cung ứng", new[] { AppRoles.Drafter }),
|
||||||
("nv.dinh@solutionerp.local", "Đinh Thị Yến", "FIN", "Nhân viên Tài chính", new[] { AppRoles.Drafter }),
|
("nv.dinh@solutions.com.vn", "Đinh Thị Yến", "FIN", "Nhân viên Tài chính", new[] { AppRoles.Drafter }),
|
||||||
("nv.truong@solutionerp.local", "Trương Minh Quân", "CCM", "Nhân viên Kiểm soát chi phí", new[] { AppRoles.Drafter }),
|
("nv.truong@solutions.com.vn", "Trương Minh Quân", "CCM", "Nhân viên Kiểm soát chi phí", new[] { AppRoles.Drafter }),
|
||||||
};
|
};
|
||||||
|
|
||||||
int created = 0, fixedExisting = 0, failed = 0;
|
int created = 0, fixedExisting = 0, failed = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user