This commit is contained in:
2026-07-16 09:53:14 +07:00
parent 0fc112a2e8
commit 29cec708a3
1236 changed files with 212848 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const ROOT = process.cwd();
const dashboardRoot = path.join(ROOT, 'src', 'app', 'dashboard');
const requiredGuardPattern =
/require(Employee|HRD|IT|UserManagement|SuperAdmin|Permission)DashboardAccess/;
const allowedWithoutGuard = new Set([
'src/app/dashboard/layout.tsx',
'src/app/dashboard/page.tsx',
'src/app/dashboard/overview/layout.tsx',
'src/app/dashboard/organizers/page.tsx'
]);
const legacyRoutePrefixes = [
'src/app/dashboard/billing/',
'src/app/dashboard/chat/',
'src/app/dashboard/elements/',
'src/app/dashboard/exclusive/',
'src/app/dashboard/forms/',
'src/app/dashboard/kanban/',
'src/app/dashboard/profile/',
'src/app/dashboard/product/',
'src/app/dashboard/react-query/',
'src/app/dashboard/workspaces/'
];
const guardedByLayoutPrefixes = ['src/app/dashboard/overview/'];
function walk(dir, results = []) {
if (!fs.existsSync(dir)) {
return results;
}
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, results);
continue;
}
if (entry.name === 'page.tsx' || entry.name === 'layout.tsx') {
results.push(fullPath);
}
}
return results;
}
function isLegacy(relativePath) {
return legacyRoutePrefixes.some((prefix) => relativePath.startsWith(prefix));
}
function isGuardedByParentLayout(relativePath) {
return guardedByLayoutPrefixes.some(
(prefix) => relativePath.startsWith(prefix) && relativePath !== `${prefix}layout.tsx`
);
}
function main() {
const files = walk(dashboardRoot);
const missingGuards = [];
for (const filePath of files) {
const relativePath = path.relative(ROOT, filePath).replace(/\\/g, '/');
if (
allowedWithoutGuard.has(relativePath) ||
isLegacy(relativePath) ||
isGuardedByParentLayout(relativePath)
) {
continue;
}
const content = fs.readFileSync(filePath, 'utf8');
if (!requiredGuardPattern.test(content)) {
missingGuards.push(relativePath);
}
}
if (missingGuards.length === 0) {
console.log('check-architecture: passed');
return;
}
console.error('check-architecture: dashboard entrypoints missing shared page guards');
for (const relativePath of missingGuards) {
console.error(`- ${relativePath}`);
}
process.exitCode = 1;
}
main();

141
scripts/check-routes.js Normal file
View File

@@ -0,0 +1,141 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const ROOT = process.cwd();
const dashboardRoot = path.join(ROOT, 'src', 'app', 'dashboard');
const scanRoots = [
path.join(ROOT, 'src', 'app'),
path.join(ROOT, 'src', 'features'),
path.join(ROOT, 'src', 'config')
];
const legacyNavRoutes = [
'/dashboard/billing',
'/dashboard/chat',
'/dashboard/elements/icons',
'/dashboard/exclusive',
'/dashboard/forms',
'/dashboard/kanban',
'/dashboard/product',
'/dashboard/react-query',
'/dashboard/workspaces'
];
function walk(dir, predicate = () => true, results = []) {
if (!fs.existsSync(dir)) {
return results;
}
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, predicate, results);
continue;
}
if (predicate(fullPath)) {
results.push(fullPath);
}
}
return results;
}
function toRoutePattern(filePath) {
const relative = path.relative(dashboardRoot, filePath).replace(/\\/g, '/');
const withoutPage = relative.replace(/\/page\.tsx$/, '').replace(/^page\.tsx$/, '');
const routePath = withoutPage ? `/dashboard/${withoutPage}` : '/dashboard';
return routePath.split('/').filter(Boolean);
}
function matchesRoute(routePattern, routePath) {
const targetSegments = routePath.split('/').filter(Boolean);
if (routePattern.length !== targetSegments.length) {
return false;
}
return routePattern.every((segment, index) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
return targetSegments[index].length > 0;
}
return segment === targetSegments[index];
});
}
function collectRoutePatterns() {
return walk(dashboardRoot, (filePath) => filePath.endsWith(`${path.sep}page.tsx`)).map(
toRoutePattern
);
}
function collectDashboardReferences() {
const routeRegex = /\/dashboard(?:\/[A-Za-z0-9-_[\]/]+)?/g;
const references = new Map();
for (const root of scanRoots) {
const files = walk(root, (filePath) => /\.(ts|tsx|js|jsx|md)$/.test(filePath));
for (const filePath of files) {
const content = fs.readFileSync(filePath, 'utf8');
const matches = content.match(routeRegex) ?? [];
for (const match of matches) {
const normalized = match.replace(/\/+$/, '');
if (!references.has(normalized)) {
references.set(normalized, []);
}
references.get(normalized).push(path.relative(ROOT, filePath).replace(/\\/g, '/'));
}
}
}
return references;
}
function main() {
const routePatterns = collectRoutePatterns();
const references = collectDashboardReferences();
const missingRoutes = [];
for (const [routePath, files] of references.entries()) {
const exists = routePatterns.some((pattern) => matchesRoute(pattern, routePath));
if (!exists) {
missingRoutes.push({ routePath, files });
}
}
const navConfigPath = path.join(ROOT, 'src', 'config', 'nav-config.ts');
const navConfig = fs.existsSync(navConfigPath) ? fs.readFileSync(navConfigPath, 'utf8') : '';
const leakedLegacyRoutes = legacyNavRoutes.filter((routePath) => navConfig.includes(routePath));
if (missingRoutes.length === 0 && leakedLegacyRoutes.length === 0) {
console.log('check-routes: passed');
return;
}
if (missingRoutes.length > 0) {
console.error('check-routes: missing dashboard route targets detected');
for (const { routePath, files } of missingRoutes) {
console.error(`- ${routePath}`);
console.error(` referenced from: ${files.join(', ')}`);
}
}
if (leakedLegacyRoutes.length > 0) {
console.error('check-routes: legacy/demo routes are still present in nav-config');
for (const routePath of leakedLegacyRoutes) {
console.error(`- ${routePath}`);
}
}
process.exitCode = 1;
}
main();

1146
scripts/cleanup.js Normal file

File diff suppressed because it is too large Load Diff

78
scripts/postinstall.js Normal file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const cleanupPath = path.join(__dirname, 'cleanup.js');
// If cleanup.js is gone, self-clean: remove this file and restore dev script
if (!fs.existsSync(cleanupPath)) {
try {
const pkgPath = path.join(__dirname, '..', 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (pkg.scripts?.dev?.includes('postinstall.js')) {
pkg.scripts.dev = 'next dev';
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
}
fs.unlinkSync(__filename);
} catch (_) {}
process.exit(0);
}
// ── ANSI colors ─────────────────────────────────────────────────────
const c = {
r: '\x1b[0m', // reset
d: '\x1b[2m', // dim
b: '\x1b[1m', // bold
cyan: '\x1b[36m',
mag: '\x1b[35m',
yel: '\x1b[33m',
grn: '\x1b[32m',
wht: '\x1b[37m',
bgCyan: '\x1b[46m\x1b[30m'
};
// ── Box drawing (ANSI-safe padding) ─────────────────────────────────
const W = 64;
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
const pad = (s, w) => s + ' '.repeat(Math.max(0, w - strip(s).length));
const row = (text = '') => `${c.d}${c.r} ${pad(text, W - 4)} ${c.d}${c.r}`;
const top = `${c.d}${'─'.repeat(W - 2)}${c.r}`;
const bot = `${c.d}${'─'.repeat(W - 2)}${c.r}`;
const div = `${c.d}${'─'.repeat(W - 2)}${c.r}`;
// ── Message ─────────────────────────────────────────────────────────
const msg = [
'',
top,
row(),
row(`${c.b}${c.mag} ✦ Shadcn Dashboard Starter${c.r}`),
row(`${c.d} Feature Cleanup Available${c.r}`),
row(),
div,
row(),
row(`${c.wht}Trim optional features to fit your project:${c.r}`),
row(),
row(` ${c.yel}$${c.r} ${c.b}node scripts/cleanup.js --interactive${c.r}`),
row(),
row(`${c.d}Available modules:${c.r}`),
row(
` ${c.cyan}clerk${c.d} · ${c.cyan}kanban${c.d} · ${c.cyan}chat${c.d} · ${c.cyan}notifications${c.d} · ${c.cyan}themes${c.d} · ${c.cyan}sentry${c.r}`
),
row(),
div,
row(),
row(`${c.grn}--dry-run${c.r} ${c.d}Preview changes without modifying files${c.r}`),
row(`${c.grn}--list${c.r} ${c.d}Show all removable features${c.r}`),
row(`${c.grn}--help${c.r} ${c.d}See all options${c.r}`),
row(),
row(`${c.d}Delete ${c.wht}scripts/cleanup.js${c.d} to remove this message.${c.r}`),
row(),
bot,
''
];
console.log(msg.join('\n'));

119
scripts/reset-db.js Normal file
View File

@@ -0,0 +1,119 @@
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const postgres = require('postgres');
function readEnvFile(filePath) {
if (!fs.existsSync(filePath)) {
return {};
}
const content = fs.readFileSync(filePath, 'utf8');
return Object.fromEntries(
content
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#') && line.includes('='))
.map((line) => {
const index = line.indexOf('=');
const key = line.slice(0, index).trim();
const value = line.slice(index + 1).trim();
return [key, value];
})
);
}
function resolveDatabaseUrl() {
if (process.env.DATABASE_URL) {
return process.env.DATABASE_URL;
}
const rootDir = process.cwd();
const envLocal = readEnvFile(path.join(rootDir, '.env.local'));
if (envLocal.DATABASE_URL) {
return envLocal.DATABASE_URL;
}
const env = readEnvFile(path.join(rootDir, '.env'));
if (env.DATABASE_URL) {
return env.DATABASE_URL;
}
throw new Error('DATABASE_URL was not found in process.env, .env.local, or .env');
}
function getMigrationFiles(rootDir) {
return fs
.readdirSync(path.join(rootDir, 'drizzle'))
.filter((file) => file.endsWith('.sql'))
.sort((left, right) => left.localeCompare(right));
}
async function main() {
const databaseUrl = resolveDatabaseUrl();
const databaseName = new URL(databaseUrl).pathname.replace(/^\//, '');
const rootDir = process.cwd();
const migrationFiles = getMigrationFiles(rootDir);
if (!databaseName) {
throw new Error('DATABASE_URL must include a database name');
}
const sql = postgres(databaseUrl, {
max: 1,
prepare: false
});
console.log(`Resetting database "${databaseName}"...`);
try {
await sql`
select pg_terminate_backend(pid)
from pg_stat_activity
where datname = current_database()
and pid <> pg_backend_pid()
`;
await sql.unsafe('drop schema if exists drizzle cascade');
await sql.unsafe('drop schema if exists public cascade');
await sql.unsafe('create schema public');
await sql.unsafe('grant all on schema public to public');
await sql.unsafe('grant all on schema public to postgres');
await sql.unsafe('create schema drizzle');
await sql.unsafe(`
create table drizzle.__drizzle_migrations (
id serial primary key,
hash text not null,
created_at bigint not null
)
`);
for (const fileName of migrationFiles) {
const filePath = path.join(rootDir, 'drizzle', fileName);
const sqlContent = fs.readFileSync(filePath, 'utf8');
const statements = sqlContent
.split('--> statement-breakpoint')
.map((statement) => statement.trim())
.filter(Boolean);
for (const statement of statements) {
await sql.unsafe(statement);
}
const hash = crypto.createHash('sha256').update(sqlContent).digest('hex');
await sql`
insert into drizzle.__drizzle_migrations (hash, created_at)
values (${hash}, ${String(Date.now())})
`;
}
} finally {
await sql.end();
}
console.log('Database reset completed.');
}
main().catch((error) => {
console.error('Failed to reset database.');
console.error(error);
process.exit(1);
});

View File

@@ -0,0 +1,91 @@
const postgres = require("postgres");
const DEFAULT_DEPARTMENTS = [
[
"HR",
"Human Resources",
"People operations, recruitment, and employee relations.",
],
[
"QA",
"Quality Assurance",
"Quality systems, audits, and compliance oversight.",
],
["PRD", "Production", "Manufacturing, operations, and production planning."],
[
"ENG",
"Engineering",
"Technical development, maintenance, and process improvement.",
],
[
"LOG",
"Logistics",
"Warehouse, shipping, receiving, and inventory coordination.",
],
["FIN", "Finance", "Accounting, budgeting, and financial control."],
];
const DEFAULT_POSITIONS = [
["MGR", "Manager", "Leads team execution, planning, and performance."],
["SUP", "Supervisor", "Oversees day-to-day operations and frontline staff."],
[
"SNR",
"Senior Staff",
"Experienced individual contributor with advanced responsibility.",
],
[
"STF",
"Staff",
"Core contributor responsible for standard operational work.",
],
["JNR", "Junior Staff", "Entry-level contributor working under guidance."],
["TRN", "Trainee", "Newly onboarded employee in structured training period."],
];
async function main() {
const connectionString =
"postgres://postgres:admin1234@localhost:5432/training_system";
if (!connectionString) {
throw new Error("DATABASE_URL is required");
}
const sql = postgres(connectionString, { prepare: false });
try {
const organizations = await sql`
select id, name
from organizations
order by created_at asc
`;
for (const organization of organizations) {
for (const [code, name, description] of DEFAULT_DEPARTMENTS) {
await sql`
insert into departments (organization_id, code, name, description)
values (${organization.id}, ${code}, ${name}, ${description})
on conflict (organization_id, name) do nothing
`;
}
for (const [code, name, description] of DEFAULT_POSITIONS) {
await sql`
insert into positions (organization_id, code, name, description)
values (${organization.id}, ${code}, ${name}, ${description})
on conflict (organization_id, name) do nothing
`;
}
}
console.log(
`Seeded master data for ${organizations.length} organization(s).`,
);
} finally {
await sql.end({ timeout: 1 });
}
}
main().catch((error) => {
console.error("Failed to seed master data.", error);
process.exitCode = 1;
});

View File

@@ -0,0 +1,82 @@
const postgres = require("postgres");
const { hash } = require("bcryptjs");
const crypto = require("node:crypto");
async function main() {
const connectionString =
"postgres://postgres:admin1234@localhost:5432/training_system";
if (!connectionString) {
throw new Error("DATABASE_URL is required");
}
const email = (
process.env.SUPER_ADMIN_EMAIL || "superadmin@training-system.local"
)
.trim()
.toLowerCase();
const name = "Super Admin".trim();
const password = "SuperAdmin123!".trim();
if (password.length < 8) {
throw new Error("SUPER_ADMIN_PASSWORD must be at least 8 characters");
}
const sql = postgres(connectionString, { prepare: false });
try {
const passwordHash = await hash(password, 10);
const existingUser = await sql`
select id
from users
where email = ${email}
limit 1
`;
if (existingUser.length > 0) {
await sql`
update users
set
name = ${name},
password_hash = ${passwordHash},
system_role = 'super_admin',
is_active = true,
updated_at = now()
where id = ${existingUser[0].id}
`;
console.log(`Updated existing super admin: ${email}`);
return;
}
const userId = crypto.randomUUID();
await sql`
insert into users (
id,
name,
email,
password_hash,
system_role,
is_active
)
values (
${userId},
${name},
${email},
${passwordHash},
'super_admin',
true
)
`;
console.log(`Created super admin: ${email}`);
} finally {
await sql.end({ timeout: 1 });
}
}
main().catch((error) => {
console.error("Failed to seed super admin.", error);
process.exitCode = 1;
});

1406
scripts/seed-uat-data.js Normal file

File diff suppressed because it is too large Load Diff