[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})`}>
|
||||
<ItemsTab ev={evaluation} readOnly={readOnly} />
|
||||
</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>
|
||||
)
|
||||
@ -869,3 +876,140 @@ function SupplierAttachmentsCell({
|
||||
</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,
|
||||
RequirementSpec: 2,
|
||||
DecisionExport: 3,
|
||||
ComparisonTable: 4,
|
||||
Other: 99,
|
||||
} as const
|
||||
|
||||
@ -128,6 +129,7 @@ export const PeAttachmentPurposeLabel: Record<number, string> = {
|
||||
1: 'Báo giá',
|
||||
2: 'Yêu cầu KT',
|
||||
3: 'Phiếu duyệt',
|
||||
4: 'Bảng so sánh',
|
||||
99: 'Khác',
|
||||
}
|
||||
|
||||
|
||||
@ -104,6 +104,13 @@ export function PeDetailTabs({
|
||||
<Section title={`Hạng mục + Báo giá (${evaluation.details.length})`}>
|
||||
<ItemsTab ev={evaluation} readOnly={readOnly} />
|
||||
</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>
|
||||
)
|
||||
@ -869,3 +876,140 @@ function SupplierAttachmentsCell({
|
||||
</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,
|
||||
RequirementSpec: 2,
|
||||
DecisionExport: 3,
|
||||
ComparisonTable: 4,
|
||||
Other: 99,
|
||||
} as const
|
||||
|
||||
@ -128,6 +129,7 @@ export const PeAttachmentPurposeLabel: Record<number, string> = {
|
||||
1: 'Báo giá',
|
||||
2: 'Yêu cầu KT',
|
||||
3: 'Phiếu duyệt',
|
||||
4: 'Bảng so sánh',
|
||||
99: 'Khác',
|
||||
}
|
||||
|
||||
|
||||
@ -4,9 +4,10 @@ namespace SolutionErp.Domain.PurchaseEvaluations;
|
||||
|
||||
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
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
@ -14,9 +14,39 @@ namespace SolutionErp.Infrastructure.Persistence;
|
||||
|
||||
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";
|
||||
|
||||
// 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)
|
||||
{
|
||||
using var scope = services.CreateScope();
|
||||
@ -30,6 +60,9 @@ public static class DbInitializer
|
||||
await db.Database.MigrateAsync();
|
||||
|
||||
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 SeedDepartmentsAsync(db, logger);
|
||||
await SeedDemoUsersAsync(db, userManager, logger);
|
||||
@ -452,10 +485,10 @@ public static class DbInitializer
|
||||
var fallbackSupplier = suppliersByCode.Values.First();
|
||||
var fallbackProject = projectsByCode.Values.First();
|
||||
|
||||
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutionerp.local");
|
||||
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutionerp.local");
|
||||
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutionerp.local");
|
||||
var hraDang = await userManager.FindByEmailAsync("hra.dang@solutionerp.local");
|
||||
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutions.com.vn");
|
||||
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutions.com.vn");
|
||||
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutions.com.vn");
|
||||
var hraDang = await userManager.FindByEmailAsync("hra.dang@solutions.com.vn");
|
||||
var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id;
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
|
||||
@ -704,11 +737,11 @@ public static class DbInitializer
|
||||
}
|
||||
|
||||
// Lookup actor users (per role)
|
||||
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutionerp.local");
|
||||
var proPham = await userManager.FindByEmailAsync("pro.pham@solutionerp.local");
|
||||
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutionerp.local");
|
||||
var pmNguyen = await userManager.FindByEmailAsync("pm.nguyen@solutionerp.local");
|
||||
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutionerp.local");
|
||||
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutions.com.vn");
|
||||
var proPham = await userManager.FindByEmailAsync("pro.pham@solutions.com.vn");
|
||||
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutions.com.vn");
|
||||
var pmNguyen = await userManager.FindByEmailAsync("pm.nguyen@solutions.com.vn");
|
||||
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutions.com.vn");
|
||||
var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id;
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
|
||||
@ -1101,28 +1134,28 @@ public static class DbInitializer
|
||||
{
|
||||
// (Email, FullName, DeptCode, Position, RoleNames[])
|
||||
// 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.le@solutionerp.local", "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.huynh@solutions.com.vn", "Huỳnh Văn Hùng", "BOD", "Tổng Giám đốc", new[] { AppRoles.Director }),
|
||||
("bod.le@solutions.com.vn", "Lê Thị Mai", "BOD", "Phó Giám đốc (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.nguyen@solutionerp.local", "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.nguyen@solutions.com.vn", "Nguyễn Quốc Cường", "PM", "Giám đốc Dự án FLOCK 01", 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.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 }),
|
||||
("pro.pham@solutionerp.local", "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 }),
|
||||
("act.vu@solutionerp.local", "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 }),
|
||||
("hra.dang@solutionerp.local", "Đặng Thị Thanh", "HRA", "Trưởng phòng Nhân sự - Hành chính", new[] { AppRoles.HrAdmin, 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@solutions.com.vn", "Phạm Thị Hồng", "PRO", "Trưởng phòng Cung ứng", new[] { AppRoles.Procurement, 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@solutions.com.vn", "Vũ Thị Lan", "ACT", "Kế toán trưởng", new[] { AppRoles.Accounting, 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@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
|
||||
("qs.hoang@solutionerp.local", "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 }),
|
||||
("nv.cao@solutionerp.local", "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.truong@solutionerp.local", "Trương Minh Quân", "CCM", "Nhân viên Kiểm soát chi phí", 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@solutions.com.vn", "Ngô Thị Hà", "QS", "QS dự án FLOCK 01", new[] { AppRoles.Drafter }),
|
||||
("nv.cao@solutions.com.vn", "Cao Văn Long", "PRO", "Nhân viên Cung ứng", 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@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;
|
||||
|
||||
Reference in New Issue
Block a user