Commit 954049ba authored by Aravind RK's avatar Aravind RK

Group interview bug have corrected

parent 78c91f3e
...@@ -48,8 +48,8 @@ const userSchema = new mongoose.Schema({ ...@@ -48,8 +48,8 @@ const userSchema = new mongoose.Schema({
}, },
level: { level: {
type: String, type: String,
enum: ['beginner', 'intermediate', 'advanced', 'expert'], enum: ['Fresher', 'Intern', 'Pre final year', 'Final year'],
default: 'beginner' default: 'Fresher'
}, },
topicsOfInterest: [{ topicsOfInterest: [{
topic: { type: String, required: true }, topic: { type: String, required: true },
......
...@@ -428,9 +428,9 @@ router.put('/:id/evaluate', authorize('admin', 'hr', 'pm', 'interviewer'), async ...@@ -428,9 +428,9 @@ router.put('/:id/evaluate', authorize('admin', 'hr', 'pm', 'interviewer'), async
// ============================================================ // ============================================================
// @route PUT /api/interview/:id/decision // @route PUT /api/interview/:id/decision
// @desc Set final decision (accepted/rejected/on_hold/2nd_round) // @desc Set final decision (accepted/rejected/on_hold/2nd_round)
// @access Admin only // @access Admin, HR
// ============================================================ // ============================================================
router.put('/:id/decision', authorize('admin'), async (req, res) => { router.put('/:id/decision', authorize('admin', 'hr'), async (req, res) => {
try { try {
const { decision } = req.body; const { decision } = req.body;
if (!decision) return res.status(400).json({ message: 'Decision is required' }); if (!decision) return res.status(400).json({ message: 'Decision is required' });
...@@ -515,9 +515,9 @@ router.put('/:id/validate-coding', authorize('admin', 'hr', 'pm', 'interviewer') ...@@ -515,9 +515,9 @@ router.put('/:id/validate-coding', authorize('admin', 'hr', 'pm', 'interviewer')
// ============================================================ // ============================================================
// @route DELETE /api/interview/:id // @route DELETE /api/interview/:id
// @desc Delete an interview // @desc Delete an interview
// @access Admin only // @access Admin, HR
// ============================================================ // ============================================================
router.delete('/:id', authorize('admin'), async (req, res) => { router.delete('/:id', authorize('admin', 'hr'), async (req, res) => {
try { try {
const interview = await Interview.findById(req.params.id); const interview = await Interview.findById(req.params.id);
if (!interview) return res.status(404).json({ message: 'Interview not found' }); if (!interview) return res.status(404).json({ message: 'Interview not found' });
......
...@@ -925,322 +925,499 @@ ...@@ -925,322 +925,499 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [ "cpu": [
"x64" "ppc64"
], ],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "aix"
], ],
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@gar/promise-retry": { "node_modules/@esbuild/android-arm": {
"version": "1.0.3", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
"integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [
"arm"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": { "engines": {
"node": "^20.17.0 || >=22.9.0" "node": ">=18"
} }
}, },
"node_modules/@harperfast/extended-iterable": { "node_modules/@esbuild/android-arm64": {
"version": "1.0.3", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
"integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "MIT",
"optional": true "optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
}, },
"node_modules/@hono/node-server": { "node_modules/@esbuild/android-x64": {
"version": "1.19.13", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
"integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [
"x64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18"
},
"peerDependencies": {
"hono": "^4"
} }
}, },
"node_modules/@inquirer/ansi": { "node_modules/@esbuild/darwin-arm64": {
"version": "1.0.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
"integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@inquirer/checkbox": { "node_modules/@esbuild/darwin-x64": {
"version": "4.3.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
"integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [
"x64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@inquirer/ansi": "^1.0.2", "os": [
"@inquirer/core": "^10.3.2", "darwin"
"@inquirer/figures": "^1.0.15", ],
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
},
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
} }
}, },
"node_modules/@inquirer/confirm": { "node_modules/@esbuild/freebsd-arm64": {
"version": "5.1.21", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
"integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@inquirer/core": "^10.3.2", "os": [
"@inquirer/type": "^3.0.10" "freebsd"
}, ],
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
} }
}, },
"node_modules/@inquirer/core": { "node_modules/@esbuild/freebsd-x64": {
"version": "10.3.2", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
"integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [
"x64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@inquirer/ansi": "^1.0.2", "os": [
"@inquirer/figures": "^1.0.15", "freebsd"
"@inquirer/type": "^3.0.10", ],
"cli-width": "^4.1.0",
"mute-stream": "^2.0.0",
"signal-exit": "^4.1.0",
"wrap-ansi": "^6.2.0",
"yoctocolors-cjs": "^2.1.3"
},
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
} }
}, },
"node_modules/@inquirer/editor": { "node_modules/@esbuild/linux-arm": {
"version": "4.2.23", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
"integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [
"arm"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@inquirer/core": "^10.3.2", "os": [
"@inquirer/external-editor": "^1.0.3", "linux"
"@inquirer/type": "^3.0.10" ],
},
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
} }
}, },
"node_modules/@inquirer/expand": { "node_modules/@esbuild/linux-arm64": {
"version": "4.0.23", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
"integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@inquirer/core": "^10.3.2", "os": [
"@inquirer/type": "^3.0.10", "linux"
"yoctocolors-cjs": "^2.1.3" ],
},
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
} }
}, },
"node_modules/@inquirer/external-editor": { "node_modules/@esbuild/linux-ia32": {
"version": "1.0.3", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [
"ia32"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"chardet": "^2.1.1", "os": [
"iconv-lite": "^0.7.0" "linux"
}, ],
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
} }
}, },
"node_modules/@inquirer/figures": { "node_modules/@esbuild/linux-loong64": {
"version": "1.0.15", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [
"loong64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@inquirer/input": { "node_modules/@esbuild/linux-mips64el": {
"version": "4.3.1", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
"integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [
"mips64el"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@inquirer/core": "^10.3.2", "os": [
"@inquirer/type": "^3.0.10" "linux"
}, ],
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
} }
}, },
"node_modules/@inquirer/number": { "node_modules/@esbuild/linux-ppc64": {
"version": "3.0.23", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
"integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [
"ppc64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@inquirer/core": "^10.3.2", "os": [
"@inquirer/type": "^3.0.10" "linux"
}, ],
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}
}, },
"peerDependencies": { "node_modules/@esbuild/linux-riscv64": {
"@types/node": ">=18" "version": "0.27.3",
}, "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
"peerDependenciesMeta": { "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"@types/node": { "cpu": [
"optional": true "riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
} }
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@inquirer/password": { "node_modules/@esbuild/linux-x64": {
"version": "4.0.23", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
"integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [
"x64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@inquirer/ansi": "^1.0.2", "os": [
"@inquirer/core": "^10.3.2", "linux"
"@inquirer/type": "^3.0.10" ],
"engines": {
"node": ">=18"
}
}, },
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}
}, },
"peerDependencies": { "node_modules/@esbuild/netbsd-x64": {
"@types/node": ">=18" "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
}, },
"peerDependenciesMeta": { "node_modules/@esbuild/openbsd-arm64": {
"@types/node": { "version": "0.27.3",
"optional": true "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
} }
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@inquirer/prompts": { "node_modules/@esbuild/openharmony-arm64": {
"version": "7.10.1", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
"integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "optional": true,
"@inquirer/checkbox": "^4.3.2", "os": [
"@inquirer/confirm": "^5.1.21", "openharmony"
"@inquirer/editor": "^4.2.23", ],
"@inquirer/expand": "^4.0.23", "engines": {
"@inquirer/input": "^4.3.1", "node": ">=18"
"@inquirer/number": "^3.0.23", }
"@inquirer/password": "^4.0.23",
"@inquirer/rawlist": "^4.1.11",
"@inquirer/search": "^3.2.2",
"@inquirer/select": "^4.4.2"
}, },
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}
}, },
"peerDependencies": { "node_modules/@esbuild/win32-arm64": {
"@types/node": ">=18" "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
}, },
"peerDependenciesMeta": { "node_modules/@esbuild/win32-ia32": {
"@types/node": { "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@gar/promise-retry": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz",
"integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@harperfast/extended-iterable": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz",
"integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==",
"dev": true,
"license": "Apache-2.0",
"optional": true "optional": true
},
"node_modules/@hono/node-server": {
"version": "1.19.13",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
"integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.14.1"
},
"peerDependencies": {
"hono": "^4"
} }
},
"node_modules/@inquirer/ansi": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
"integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
} }
}, },
"node_modules/@inquirer/rawlist": { "node_modules/@inquirer/checkbox": {
"version": "4.1.11", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz",
"integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.2", "@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10", "@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3" "yoctocolors-cjs": "^2.1.3"
}, },
...@@ -1256,17 +1433,15 @@ ...@@ -1256,17 +1433,15 @@
} }
} }
}, },
"node_modules/@inquirer/search": { "node_modules/@inquirer/confirm": {
"version": "3.2.2", "version": "5.1.21",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
"integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@inquirer/core": "^10.3.2", "@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10"
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
...@@ -1280,17 +1455,20 @@ ...@@ -1280,17 +1455,20 @@
} }
} }
}, },
"node_modules/@inquirer/select": { "node_modules/@inquirer/core": {
"version": "4.4.2", "version": "10.3.2",
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
"integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@inquirer/ansi": "^1.0.2", "@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15", "@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10", "@inquirer/type": "^3.0.10",
"cli-width": "^4.1.0",
"mute-stream": "^2.0.0",
"signal-exit": "^4.1.0",
"wrap-ansi": "^6.2.0",
"yoctocolors-cjs": "^2.1.3" "yoctocolors-cjs": "^2.1.3"
}, },
"engines": { "engines": {
...@@ -1305,12 +1483,17 @@ ...@@ -1305,12 +1483,17 @@
} }
} }
}, },
"node_modules/@inquirer/type": { "node_modules/@inquirer/editor": {
"version": "3.0.10", "version": "4.2.23",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz",
"integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/external-editor": "^1.0.3",
"@inquirer/type": "^3.0.10"
},
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
...@@ -1323,9 +1506,251 @@ ...@@ -1323,9 +1506,251 @@
} }
} }
}, },
"node_modules/@isaacs/fs-minipass": { "node_modules/@inquirer/expand": {
"version": "4.0.1", "version": "4.0.23",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz",
"integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/external-editor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chardet": "^2.1.1",
"iconv-lite": "^0.7.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/figures": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@inquirer/input": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz",
"integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/number": {
"version": "3.0.23",
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz",
"integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/password": {
"version": "4.0.23",
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz",
"integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/prompts": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz",
"integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/checkbox": "^4.3.2",
"@inquirer/confirm": "^5.1.21",
"@inquirer/editor": "^4.2.23",
"@inquirer/expand": "^4.0.23",
"@inquirer/input": "^4.3.1",
"@inquirer/number": "^3.0.23",
"@inquirer/password": "^4.0.23",
"@inquirer/rawlist": "^4.1.11",
"@inquirer/search": "^3.2.2",
"@inquirer/select": "^4.4.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/rawlist": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz",
"integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/search": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz",
"integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/select": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz",
"integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/type": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
"integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
...@@ -1413,26 +1838,110 @@ ...@@ -1413,26 +1838,110 @@
"listr2": "9.0.5" "listr2": "9.0.5"
} }
}, },
"node_modules/@lmdb/lmdb-win32-x64": { "node_modules/@lmdb/lmdb-darwin-arm64": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz",
"integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==",
"cpu": [ "cpu": [
"x64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "darwin"
] ]
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@lmdb/lmdb-darwin-x64": {
"version": "1.26.0", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz",
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==",
"dev": true, "cpu": [
"license": "MIT", "x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@lmdb/lmdb-linux-arm": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz",
"integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@lmdb/lmdb-linux-arm64": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz",
"integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@lmdb/lmdb-linux-x64": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz",
"integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@lmdb/lmdb-win32-arm64": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz",
"integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@lmdb/lmdb-win32-x64": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz",
"integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
"dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@hono/node-server": "^1.19.9", "@hono/node-server": "^1.19.9",
"ajv": "^8.17.1", "ajv": "^8.17.1",
...@@ -1468,6 +1977,76 @@ ...@@ -1468,6 +1977,76 @@
} }
} }
}, },
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
...@@ -1516,125 +2095,437 @@ ...@@ -1516,125 +2095,437 @@
"@napi-rs/nice-win32-x64-msvc": "1.1.1" "@napi-rs/nice-win32-x64-msvc": "1.1.1"
} }
}, },
"node_modules/@napi-rs/nice-win32-x64-msvc": { "node_modules/@napi-rs/nice-android-arm-eabi": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz",
"integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==",
"cpu": [ "cpu": [
"x64" "arm"
], ],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "android"
], ],
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@npmcli/agent": { "node_modules/@napi-rs/nice-android-arm64": {
"version": "4.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz",
"integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "ISC", "license": "MIT",
"dependencies": { "optional": true,
"agent-base": "^7.1.0", "os": [
"http-proxy-agent": "^7.0.0", "android"
"https-proxy-agent": "^7.0.1", ],
"lru-cache": "^11.2.1",
"socks-proxy-agent": "^8.0.3"
},
"engines": { "engines": {
"node": "^20.17.0 || >=22.9.0" "node": ">= 10"
} }
}, },
"node_modules/@npmcli/agent/node_modules/lru-cache": { "node_modules/@napi-rs/nice-darwin-arm64": {
"version": "11.3.2", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.2.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz",
"integrity": "sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==", "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": { "engines": {
"node": "20 || >=22" "node": ">= 10"
} }
}, },
"node_modules/@npmcli/fs": { "node_modules/@napi-rs/nice-darwin-x64": {
"version": "5.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz",
"integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==",
"cpu": [
"x64"
],
"dev": true, "dev": true,
"license": "ISC", "license": "MIT",
"dependencies": { "optional": true,
"semver": "^7.3.5" "os": [
}, "darwin"
],
"engines": { "engines": {
"node": "^20.17.0 || >=22.9.0" "node": ">= 10"
} }
}, },
"node_modules/@npmcli/git": { "node_modules/@napi-rs/nice-freebsd-x64": {
"version": "7.0.2", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz",
"integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==",
"cpu": [
"x64"
],
"dev": true, "dev": true,
"license": "ISC", "license": "MIT",
"dependencies": { "optional": true,
"@gar/promise-retry": "^1.0.0", "os": [
"@npmcli/promise-spawn": "^9.0.0", "freebsd"
"ini": "^6.0.0", ],
"lru-cache": "^11.2.1",
"npm-pick-manifest": "^11.0.1",
"proc-log": "^6.0.0",
"semver": "^7.3.5",
"which": "^6.0.0"
},
"engines": { "engines": {
"node": "^20.17.0 || >=22.9.0" "node": ">= 10"
} }
}, },
"node_modules/@npmcli/git/node_modules/isexe": { "node_modules/@napi-rs/nice-linux-arm-gnueabihf": {
"version": "4.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz",
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==",
"cpu": [
"arm"
],
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": { "engines": {
"node": ">=20" "node": ">= 10"
} }
}, },
"node_modules/@npmcli/git/node_modules/lru-cache": { "node_modules/@napi-rs/nice-linux-arm64-gnu": {
"version": "11.3.2", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.2.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz",
"integrity": "sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==", "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": { "engines": {
"node": "20 || >=22" "node": ">= 10"
} }
}, },
"node_modules/@npmcli/git/node_modules/which": { "node_modules/@napi-rs/nice-linux-arm64-musl": {
"version": "6.0.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz",
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "ISC", "libc": [
"dependencies": { "musl"
"isexe": "^4.0.0" ],
}, "license": "MIT",
"bin": { "optional": true,
"node-which": "bin/which.js" "os": [
}, "linux"
],
"engines": { "engines": {
"node": "^20.17.0 || >=22.9.0" "node": ">= 10"
} }
}, },
"node_modules/@npmcli/installed-package-contents": { "node_modules/@napi-rs/nice-linux-ppc64-gnu": {
"version": "4.0.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz",
"integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==",
"cpu": [
"ppc64"
],
"dev": true, "dev": true,
"license": "ISC", "libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/nice-linux-riscv64-gnu": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz",
"integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/nice-linux-s390x-gnu": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz",
"integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/nice-linux-x64-gnu": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz",
"integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/nice-linux-x64-musl": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz",
"integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/nice-openharmony-arm64": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz",
"integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/nice-win32-arm64-msvc": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz",
"integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/nice-win32-ia32-msvc": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz",
"integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/nice-win32-x64-msvc": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz",
"integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@npmcli/agent": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz",
"integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==",
"dev": true,
"license": "ISC",
"dependencies": {
"agent-base": "^7.1.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.1",
"lru-cache": "^11.2.1",
"socks-proxy-agent": "^8.0.3"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@npmcli/agent/node_modules/lru-cache": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.2.tgz",
"integrity": "sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@npmcli/fs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz",
"integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==",
"dev": true,
"license": "ISC",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@npmcli/git": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz",
"integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==",
"dev": true,
"license": "ISC",
"dependencies": {
"@gar/promise-retry": "^1.0.0",
"@npmcli/promise-spawn": "^9.0.0",
"ini": "^6.0.0",
"lru-cache": "^11.2.1",
"npm-pick-manifest": "^11.0.1",
"proc-log": "^6.0.0",
"semver": "^7.3.5",
"which": "^6.0.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@npmcli/git/node_modules/isexe": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=20"
}
},
"node_modules/@npmcli/git/node_modules/lru-cache": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.2.tgz",
"integrity": "sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@npmcli/git/node_modules/which": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^4.0.0"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@npmcli/installed-package-contents": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz",
"integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==",
"dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"npm-bundled": "^5.0.0", "npm-bundled": "^5.0.0",
"npm-normalize-package-bin": "^5.0.0" "npm-normalize-package-bin": "^5.0.0"
...@@ -1693,153 +2584,1000 @@ ...@@ -1693,153 +2584,1000 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
"dev": true, "dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"engines": { "engines": {
"node": ">=20" "node": ">=20"
} }
},
"node_modules/@npmcli/promise-spawn/node_modules/which": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^4.0.0"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@npmcli/redact": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz",
"integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@npmcli/run-script": {
"version": "10.0.4",
"resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz",
"integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==",
"dev": true,
"license": "ISC",
"dependencies": {
"@npmcli/node-gyp": "^5.0.0",
"@npmcli/package-json": "^7.0.0",
"@npmcli/promise-spawn": "^9.0.0",
"node-gyp": "^12.1.0",
"proc-log": "^6.0.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@oxc-project/types": {
"version": "0.113.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz",
"integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.6",
"@parcel/watcher-darwin-arm64": "2.5.6",
"@parcel/watcher-darwin-x64": "2.5.6",
"@parcel/watcher-freebsd-x64": "2.5.6",
"@parcel/watcher-linux-arm-glibc": "2.5.6",
"@parcel/watcher-linux-arm-musl": "2.5.6",
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
"@parcel/watcher-linux-arm64-musl": "2.5.6",
"@parcel/watcher-linux-x64-glibc": "2.5.6",
"@parcel/watcher-linux-x64-musl": "2.5.6",
"@parcel/watcher-win32-arm64": "2.5.6",
"@parcel/watcher-win32-ia32": "2.5.6",
"@parcel/watcher-win32-x64": "2.5.6"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
"cpu": [
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
"cpu": [
"arm"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher/node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz",
"integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz",
"integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz",
"integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz",
"integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz",
"integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz",
"integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz",
"integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz",
"integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz",
"integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz",
"integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz",
"integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "^1.1.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz",
"integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz",
"integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz",
"integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
}, },
"node_modules/@npmcli/promise-spawn/node_modules/which": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "6.0.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
"cpu": [
"arm"
],
"dev": true, "dev": true,
"license": "ISC", "libc": [
"dependencies": { "glibc"
"isexe": "^4.0.0" ],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
}, },
"bin": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"node-which": "bin/which.js" "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
"cpu": [
"arm"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
}, },
"engines": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"node": "^20.17.0 || >=22.9.0" "version": "4.60.1",
} "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
}, },
"node_modules/@npmcli/redact": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.0.0", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
"integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "ISC", "libc": [
"engines": { "musl"
"node": "^20.17.0 || >=22.9.0" ],
} "license": "MIT",
"optional": true,
"os": [
"linux"
]
}, },
"node_modules/@npmcli/run-script": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "10.0.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
"integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
"cpu": [
"loong64"
],
"dev": true, "dev": true,
"license": "ISC", "libc": [
"dependencies": { "glibc"
"@npmcli/node-gyp": "^5.0.0", ],
"@npmcli/package-json": "^7.0.0", "license": "MIT",
"@npmcli/promise-spawn": "^9.0.0", "optional": true,
"node-gyp": "^12.1.0", "os": [
"proc-log": "^6.0.0" "linux"
]
}, },
"engines": { "node_modules/@rollup/rollup-linux-loong64-musl": {
"node": "^20.17.0 || >=22.9.0" "version": "4.60.1",
} "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
"cpu": [
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
}, },
"node_modules/@oxc-project/types": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "0.113.0", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
"integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
"cpu": [
"ppc64"
],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"funding": { "optional": true,
"url": "https://github.com/sponsors/Boshen" "os": [
} "linux"
]
}, },
"node_modules/@parcel/watcher": { "node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "2.5.6", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
"cpu": [
"ppc64"
],
"dev": true, "dev": true,
"hasInstallScript": true, "libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "os": [
"detect-libc": "^2.0.3", "linux"
"is-glob": "^4.0.3", ]
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
}, },
"engines": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"node": ">= 10.0.0" "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
}, },
"funding": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"type": "opencollective", "version": "4.60.1",
"url": "https://opencollective.com/parcel" "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
}, },
"optionalDependencies": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"@parcel/watcher-android-arm64": "2.5.6", "version": "4.60.1",
"@parcel/watcher-darwin-arm64": "2.5.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
"@parcel/watcher-darwin-x64": "2.5.6", "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
"@parcel/watcher-freebsd-x64": "2.5.6", "cpu": [
"@parcel/watcher-linux-arm-glibc": "2.5.6", "s390x"
"@parcel/watcher-linux-arm-musl": "2.5.6", ],
"@parcel/watcher-linux-arm64-glibc": "2.5.6", "dev": true,
"@parcel/watcher-linux-arm64-musl": "2.5.6", "libc": [
"@parcel/watcher-linux-x64-glibc": "2.5.6", "glibc"
"@parcel/watcher-linux-x64-musl": "2.5.6", ],
"@parcel/watcher-win32-arm64": "2.5.6", "license": "MIT",
"@parcel/watcher-win32-ia32": "2.5.6", "optional": true,
"@parcel/watcher-win32-x64": "2.5.6" "os": [
} "linux"
]
}, },
"node_modules/@parcel/watcher-win32-x64": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "2.5.6", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
"cpu": [
"x64"
], ],
"engines": { "dev": true,
"node": ">= 10.0.0" "libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
}, },
"funding": { "node_modules/@rollup/rollup-openbsd-x64": {
"type": "opencollective", "version": "4.60.1",
"url": "https://opencollective.com/parcel" "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
} "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
}, },
"node_modules/@parcel/watcher/node_modules/node-addon-api": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "7.1.1", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
"cpu": [
"arm64"
],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true,
"os": [
"openharmony"
]
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "1.0.0-rc.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
"integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
"cpu": [ "cpu": [
"x64" "arm64"
], ],
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
], ]
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}, },
"node_modules/@rolldown/pluginutils": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "1.0.0-rc.4", "version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
"integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
"cpu": [
"ia32"
],
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"os": [
"win32"
]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.60.1", "version": "4.60.1",
...@@ -1996,6 +3734,17 @@ ...@@ -1996,6 +3734,17 @@
"node": "^20.17.0 || >=22.9.0" "node": "^20.17.0 || >=22.9.0"
} }
}, },
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
...@@ -3164,6 +4913,21 @@ ...@@ -3164,6 +4913,21 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
......
...@@ -59,12 +59,15 @@ ...@@ -59,12 +59,15 @@
/* Candidate chips row */ /* Candidate chips row */
.candidate-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; } .candidate-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.candidate-chip { .candidate-chip {
width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; padding: 4px 12px; border-radius: 16px; display: flex; align-items: center;
justify-content: center; font-size: 12px; font-weight: 700; color: #fff; justify-content: center; font-size: 12px; font-weight: 600; color: #fff!important;
border: 2px solid var(--bg-card); background: #667eea; border: 2px solid var(--bg-card); background: #667eea;
transition: transform 0.15s; transition: transform 0.15s;
white-space: nowrap;
} }
.candidate-chip:hover { transform: scale(1.15); z-index: 1; } .candidate-chip:hover { transform: scale(1.15); z-index: 1; }
.candidate-chip.clickable-chip { cursor: pointer; }
.candidate-chip.clickable-chip:hover { transform: scale(1.12); box-shadow: 0 4px 12px rgba(0,0,0,0.18); }
.candidate-chip.badge-warning { background: #f59e0b; } .candidate-chip.badge-warning { background: #f59e0b; }
.candidate-chip.badge-info { background: #3b82f6; } .candidate-chip.badge-info { background: #3b82f6; }
.candidate-chip.badge-success { background: #22c55e; } .candidate-chip.badge-success { background: #22c55e; }
...@@ -73,6 +76,29 @@ ...@@ -73,6 +76,29 @@
.iv-card-bottom { display: flex; justify-content: flex-end; } .iv-card-bottom { display: flex; justify-content: flex-end; }
.status-summary { font-size: 12px; color: var(--text-muted); font-style: italic; } .status-summary { font-size: 12px; color: var(--text-muted); font-style: italic; }
/* ⚠ badge on chip */
.chip-alert { font-size: 11px; margin-right: 3px; }
/* Evaluation pending badge inside candidate row name */
.eval-pending-badge {
display: inline-flex; align-items: center; font-size: 11px; font-weight: 600;
padding: 2px 7px; border-radius: 10px; margin-left: 8px;
background: rgba(245,158,11,0.15); color: #f59e0b;
}
.eval-pending-badge.warn { background: rgba(102,126,234,0.1); color: #667eea; }
/* Clickable candidate left section (button reset) */
.cr-clickable {
display: flex; align-items: center; gap: 12px;
background: none; border: none; cursor: pointer; text-align: left;
padding: 6px 10px; border-radius: 10px; transition: background 0.15s;
min-width: 160px; flex-shrink: 0;
}
.cr-clickable:hover { background: rgba(102,126,234,0.08); }
/* Hint text at end of bottom row */
.cr-open-hint { font-size: 11px; color: var(--text-muted); margin-left: auto; font-style: italic; }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
BADGES BADGES
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
...@@ -293,20 +319,26 @@ ...@@ -293,20 +319,26 @@
/* Candidate detail list */ /* Candidate detail list */
.candidate-detail-list { display: flex; flex-direction: column; gap: 12px; } .candidate-detail-list { display: flex; flex-direction: column; gap: 12px; }
.candidate-row { .candidate-row {
display: flex; align-items: flex-start; gap: 16px; padding: 16px; display: flex; flex-direction: column; gap: 12px; padding: 16px;
border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-hover); border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-hover);
flex-wrap: wrap; transition: border-color 0.2s; transition: border-color 0.2s;
} }
.candidate-row:hover { border-color: rgba(102,126,234,0.35); } .candidate-row:hover { border-color: rgba(102,126,234,0.35); }
.candidate-row-left { display: flex; align-items: center; gap: 12px; min-width: 180px; } /* Top row: avatar+name on left, progress stepper fills the rest */
.candidate-row-top { display: flex; align-items: center; gap: 16px; }
.candidate-row-left { display: flex; align-items: center; gap: 12px; min-width: 160px; flex-shrink: 0; }
.iv-avatar { width: 40px; height: 40px; border-radius: 10px; background: linear-gradient(135deg,#667eea,#764ba2); color:#fff; font-weight:700; font-size:16px; display:flex; align-items:center; justify-content:center; flex-shrink:0; } .iv-avatar { width: 40px; height: 40px; border-radius: 10px; background: linear-gradient(135deg,#667eea,#764ba2); color:#fff; font-weight:700; font-size:16px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.iv-avatar.small { width: 32px; height: 32px; font-size: 14px; border-radius: 8px; } .iv-avatar.small { width: 36px; height: 36px; font-size: 15px; border-radius: 9px; }
.cr-name { font-size: 14px; font-weight: 600; color: var(--text-primary); } .cr-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.cr-email { font-size: 12px; color: var(--text-muted); } .cr-email { font-size: 12px; color: var(--text-muted); }
.candidate-row-mid { flex: 1; }
.candidate-row-mid { flex: 1; min-width: 280px; } /* Bottom row: status badge + quiz chips + accept/reject — all left-aligned */
.candidate-row-right { display: flex; flex-direction: column; gap: 8px; align-items: flex-end; } .candidate-row-bottom {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
padding-top: 10px; border-top: 1px solid var(--border-color);
}
/* Progress steps (mini) */ /* Progress steps (mini) */
.progress-steps { display: flex; align-items: center; } .progress-steps { display: flex; align-items: center; }
...@@ -352,6 +384,45 @@ ...@@ -352,6 +384,45 @@
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes slideUp { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
/* ═══════════════════════════════════════════════════════
EVALUATION PANEL (member detail modal)
Mirrors individual-interview styles
═══════════════════════════════════════════════════════ */
.quiz-results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.quiz-result-card {
padding: 14px 16px; border-radius: 12px; border: 1px solid var(--border-color);
background: var(--bg-input);
}
.qr-title { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; }
.qr-score { font-size: 22px; font-weight: 700; color: #22c55e; }
.qr-pending { font-size: 13px; color: var(--text-muted); font-style: italic; }
.eval-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }
.eval-card {
padding: 14px 16px; border-radius: 12px; border: 1px solid var(--border-color);
background: var(--bg-input);
}
.eval-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.eval-evaluator { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.eval-role { font-size: 10px !important; padding: 2px 6px !important; }
.eval-comments { font-size: 13px; color: var(--text-secondary); font-style: italic; margin: 6px 0 8px; }
.eval-date { font-size: 11px; color: var(--text-muted); }
.eval-form {
margin-top: 16px; padding: 18px 20px; border-radius: 14px;
border: 1px dashed rgba(102,126,234,0.35);
background: rgba(102,126,234,0.04);
display: flex; flex-direction: column; gap: 12px;
}
.eval-form h4 { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; }
.form-textarea { resize: vertical; min-height: 80px; }
.decision-section { border-bottom: none; }
.decision-buttons { display: flex; flex-wrap: wrap; gap: 10px; }
.btn-success { background: #22c55e; color: #fff; border: none; }
.btn-warning { background: #f59e0b; color: #fff; border: none; }
/* ═══════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════
RESPONSIVE RESPONSIVE
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
...@@ -365,3 +436,41 @@ ...@@ -365,3 +436,41 @@
.candidate-row-right { align-items: flex-start; } .candidate-row-right { align-items: flex-start; }
.da-header, .da-row { grid-template-columns: 1fr; } .da-header, .da-row { grid-template-columns: 1fr; }
} }
/* ═══════════════════════════════════════════════════════
PRINT STYLES FOR EVALUATION PDF
═══════════════════════════════════════════════════════ */
.print-container {
display: none;
}
@media print {
body * {
visibility: hidden;
}
.print-container, .print-container * {
visibility: visible;
}
.print-container {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
padding: 20px;
background: white;
color: black;
font-family: Arial, sans-serif;
}
.print-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #0078d4;
padding-bottom: 10px;
margin-bottom: 20px;
}
}
...@@ -93,15 +93,22 @@ ...@@ -93,15 +93,22 @@
} }
</div> </div>
<!-- Candidate progress chips --> <!-- Candidate progress chips — click any chip to open the evaluation panel directly -->
<div class="candidate-chips"> <div class="candidate-chips">
@for (m of g.members; track m._id) { @for (m of g.members; track m._id) {
<span class="candidate-chip" [ngClass]="getStatusClass(m.status)" [title]="m.candidateId?.name + ' — ' + formatStatus(m.status)"> <span class="candidate-chip clickable-chip" [ngClass]="getStatusClass(m.status)"
{{ m.candidateId?.name?.charAt(0)?.toUpperCase() }} [title]="m.candidateId?.name + ' — ' + formatStatus(m.status) + (hasPendingEvaluations(m) ? ' (Evaluations pending)' : '')"
(click)="openMemberDetail(m._id, $event)">
@if (hasPendingEvaluations(m)) {
<span class="chip-alert" title="Evaluations still pending">⚠️</span>
}
{{ m.candidateId?.name }}
</span> </span>
} }
</div> </div>
<div class="iv-card-bottom"> <div class="iv-card-bottom">
<span class="status-summary">{{ groupStatusSummary(g.members) }}</span> <span class="status-summary">{{ groupStatusSummary(g.members) }}</span>
</div> </div>
...@@ -403,15 +410,25 @@ ...@@ -403,15 +410,25 @@
<div class="candidate-detail-list"> <div class="candidate-detail-list">
@for (m of selectedGroup().members; track m._id) { @for (m of selectedGroup().members; track m._id) {
<div class="candidate-row"> <div class="candidate-row">
<div class="candidate-row-left">
<!-- Top row: clickable avatar+name on the left, progress bar on the right -->
<div class="candidate-row-top">
<button class="cr-clickable" (click)="openMemberDetail(m._id, $event)">
<div class="iv-avatar small">{{ m.candidateId?.name?.charAt(0)?.toUpperCase() }}</div> <div class="iv-avatar small">{{ m.candidateId?.name?.charAt(0)?.toUpperCase() }}</div>
<div> <div>
<div class="cr-name">{{ m.candidateId?.name }}</div> <div class="cr-name">
<div class="cr-email">{{ m.candidateId?.email }}</div> {{ m.candidateId?.name }}
@if (needsEvaluation(m)) {
<span class="eval-pending-badge" title="Your evaluation is pending">⚠️ Evaluate</span>
}
@if (hasPendingEvaluations(m)) {
<span class="eval-pending-badge warn" title="Some evaluations are still pending"></span>
}
</div> </div>
<div class="cr-email">{{ m.candidateId?.email }}</div>
</div> </div>
</button>
<div class="candidate-row-mid"> <div class="candidate-row-mid">
<!-- Progress steps mini -->
<div class="progress-steps mini"> <div class="progress-steps mini">
<div class="step" [class.done]="['quiz_phase','coding_phase','evaluation','completed'].includes(m.status)"> <div class="step" [class.done]="['quiz_phase','coding_phase','evaluation','completed'].includes(m.status)">
<span class="step-dot"></span><span class="step-label">Created</span> <span class="step-dot"></span><span class="step-label">Created</span>
...@@ -434,29 +451,24 @@ ...@@ -434,29 +451,24 @@
</div> </div>
</div> </div>
</div> </div>
<div class="candidate-row-right"> </div>
<!-- Bottom row: status badge + quiz scores (no accept/reject here) -->
<div class="candidate-row-bottom">
<span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span> <span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span>
@if (m.finalDecision !== 'pending') { @if (m.finalDecision !== 'pending') {
<span class="badge" [ngClass]="getDecisionClass(m.finalDecision)">{{ formatDecision(m.finalDecision) }}</span> <span class="badge" [ngClass]="getDecisionClass(m.finalDecision)">{{ formatDecision(m.finalDecision) }}</span>
} }
<!-- Quiz scores -->
@if (m.quizzes?.length > 0) { @if (m.quizzes?.length > 0) {
<div class="quiz-scores-inline">
@for (q of m.quizzes; track q.quizId) { @for (q of m.quizzes; track q.quizId) {
<span class="quiz-score-chip" [class.completed]="q.completed"> <span class="quiz-score-chip" [class.completed]="q.completed">
{{ q.title }}: {{ q.completed ? q.score + '/' + q.totalMarks : 'Pending' }} {{ q.title }}: {{ q.completed ? q.score + '/' + q.totalMarks : 'Pending' }}
</span> </span>
} }
</div>
}
<!-- Decision buttons (admin) -->
@if (authService.getUserRole() === 'admin' && m.status !== 'completed') {
<div class="mini-decision-btns">
<button class="mini-btn success" (click)="setDecision(m._id, 'accepted'); $event.stopPropagation()">Accept</button>
<button class="mini-btn danger" (click)="setDecision(m._id, 'rejected'); $event.stopPropagation()">Reject</button>
</div>
} }
<span class="cr-open-hint">Click name to evaluate →</span>
</div> </div>
</div> </div>
} }
</div> </div>
...@@ -465,7 +477,7 @@ ...@@ -465,7 +477,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@if (authService.getUserRole() === 'admin') { @if (authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') {
<button class="btn btn-danger btn-sm" (click)="deleteGroupInterview(selectedGroup().groupId)"> <button class="btn btn-danger btn-sm" (click)="deleteGroupInterview(selectedGroup().groupId)">
Delete All Interviews Delete All Interviews
</button> </button>
...@@ -476,3 +488,261 @@ ...@@ -476,3 +488,261 @@
</div> </div>
</div> </div>
} }
<!-- ═══════════════════════════════════════════════════════════
MEMBER DETAIL MODAL — full evaluation panel for one candidate
═══════════════════════════════════════════════════════════ -->
@if (showMemberDetailModal() && selectedMember()) {
<div class="modal-overlay" (click)="closeMemberDetail()">
<div class="modal-container modal-xl" (click)="$event.stopPropagation()">
<!-- Header -->
<div class="modal-header">
<div>
<h2>{{ selectedMember().candidateId?.name }}</h2>
<span class="badge" [ngClass]="getStatusClass(selectedMember().status)">{{ formatStatus(selectedMember().status) }}</span>
@if (selectedMember().finalDecision !== 'pending') {
<span class="badge" [ngClass]="getDecisionClass(selectedMember().finalDecision)" style="margin-left:6px">
{{ formatDecision(selectedMember().finalDecision) }}
</span>
}
</div>
<button class="icon-btn" (click)="closeMemberDetail()"><span class="material-symbols-rounded">close</span></button>
</div>
<div class="modal-body detail-body">
<!-- Candidate Info -->
<div class="detail-section">
<h3 class="detail-section-title">Candidate Information</h3>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Name</span>
<span class="detail-value">{{ selectedMember().candidateId?.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Email</span>
<span class="detail-value">{{ selectedMember().candidateId?.email }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Position</span>
<span class="detail-value">{{ selectedMember().position }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Tech Stack</span>
<span class="detail-value">{{ selectedMember().techStack || '—' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Date of Interview</span>
<span class="detail-value">{{ selectedMember().dateOfInterview | date:'mediumDate' }}</span>
</div>
</div>
</div>
<!-- Quiz Results -->
@if (selectedMember().quizzes?.length > 0) {
<div class="detail-section">
<h3 class="detail-section-title">Quiz Results</h3>
<div class="quiz-results-grid">
@for (q of selectedMember().quizzes; track q.quizId) {
<div class="quiz-result-card">
<div class="qr-title">{{ q.title }}</div>
@if (q.completed) {
<div class="qr-score">{{ q.score }}/{{ q.totalMarks }} ({{ q.percentage }}%)</div>
} @else {
<div class="qr-pending">Not Taken</div>
}
</div>
}
</div>
</div>
}
<!-- Evaluations -->
<div class="detail-section">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<h3 class="detail-section-title" style="margin:0;">Evaluations</h3>
<div style="display: flex; gap: 8px; align-items: center;">
@if (allMemberEvaluationsDone()) {
<span class="badge badge-success">All evaluations done</span>
<button class="btn btn-primary btn-sm" (click)="downloadEvaluationPdf()">
@if (isPdfGenerating()) { <span class="spinner spinner-sm"></span> Generating... } @else { <span class="material-symbols-rounded" style="font-size: 16px; margin-right: 4px;">download</span> Download PDF }
</button>
} @else {
<span class="badge badge-warning">Evaluations pending</span>
}
</div>
</div>
<!-- Evaluation list -->
@if (selectedMember().evaluations?.length > 0) {
<div class="eval-list">
@for (ev of selectedMember().evaluations; track ev._id) {
<div class="eval-card">
<div class="eval-header">
<div class="eval-evaluator">
<strong>{{ ev.evaluatorId?.name }}</strong>
<span class="eval-role badge badge-muted">{{ ev.evaluatorRole | uppercase }}</span>
</div>
<span class="badge" [ngClass]="getDecisionClass(ev.recommendation)">{{ formatDecision(ev.recommendation) }}</span>
</div>
@if (ev.comments) {
<p class="eval-comments">"{{ ev.comments }}"</p>
}
<span class="eval-date">{{ ev.date | date:'medium' }}</span>
</div>
}
</div>
} @else {
<p class="text-muted">No evaluations yet</p>
}
<!-- Add evaluation form (HR / PM / Interviewer, not if already submitted or completed) -->
@if (!hasMemberEvaluated() && selectedMember().status !== 'completed') {
<div class="eval-form">
<h4>Add Your Evaluation</h4>
<div class="form-group">
<label class="form-label">Comments</label>
<textarea class="form-input form-textarea" [(ngModel)]="memberEvalComment"
placeholder="Enter your comments..." rows="3"></textarea>
</div>
<div class="form-group">
<label class="form-label">Recommendation *</label>
<select class="form-input" [(ngModel)]="memberEvalRecommendation">
<option value="">Select recommendation</option>
<option value="offer">Offer / Hire as Intern</option>
<option value="on_hold">On Hold</option>
<option value="rejected">Rejected</option>
<option value="2nd_round">2nd Round</option>
</select>
</div>
<button class="btn btn-primary" (click)="submitMemberEvaluation()"
[disabled]="!memberEvalRecommendation || isMemberSubmitting()">
@if (isMemberSubmitting()) { <span class="spinner spinner-sm"></span> Submitting... }
@else { Submit Evaluation }
</button>
</div>
}
</div>
<!-- Final Decision — Admin and HR, only when all evaluations done -->
@if ((authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') && allMemberEvaluationsDone() && selectedMember().status !== 'completed') {
<div class="detail-section decision-section">
<h3 class="detail-section-title">Final Decision</h3>
<div class="decision-buttons">
<button class="btn btn-success" (click)="setMemberDecision('accepted')">
<span class="material-symbols-rounded">check_circle</span> Accept
</button>
<button class="btn btn-warning" (click)="setMemberDecision('on_hold')">
<span class="material-symbols-rounded">pause_circle</span> On Hold
</button>
<button class="btn btn-danger" (click)="setMemberDecision('rejected')">
<span class="material-symbols-rounded">cancel</span> Reject
</button>
<button class="btn btn-outline" (click)="setMemberDecision('2nd_round')">
<span class="material-symbols-rounded">replay</span> 2nd Round
</button>
</div>
</div>
}
</div>
<div class="modal-footer">
<button class="btn btn-outline" (click)="closeMemberDetail()">Close</button>
</div>
</div>
</div>
}
<!-- Print Template for Evaluation PDF -->
@if (selectedMember()) {
<div class="print-container">
<div class="print-header">
<div class="print-header-left">
<h2 style="margin: 0; font-size: 20px;">Intern Interview Evaluation Form</h2>
</div>
<div class="print-header-right" style="text-align: right;">
<span style="color: #0078d4; font-weight: bold; font-size: 24px;">IDEAL</span><br>
<span style="font-size: 10px; color: #555;">TECH LABS</span>
</div>
</div>
<table class="print-table" style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold; width: 25%;">Candidate Name:</td>
<td style="border: 1px solid #000; padding: 8px; width: 25%;">{{ selectedMember().candidateId?.name }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold; width: 25%;">Date of Interview:</td>
<td style="border: 1px solid #000; padding: 8px; width: 25%;">{{ selectedMember().dateOfInterview | date:'mediumDate' }}</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Position:</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().position }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Source</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().source || '—' }}</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Tech Stack:</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().techStack || '—' }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Interviewer(s):</td>
<td style="border: 1px solid #000; padding: 8px;">
@if (selectedMember().assignedInterviewers?.length) {
@for (i of selectedMember().assignedInterviewers; track i._id; let last = $last) {
{{ i.name }}{{ !last ? ', ' : '' }}
}
} @else {
{{ selectedMember().interviewerId?.name || '—' }}
}
</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">General Aptitude Test QP Set</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().quizzes?.[0]?.title || '—' }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">General Aptitude Test Score</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().quizzes?.[0]?.completed ? selectedMember().quizzes[0].score + '/' + selectedMember().quizzes[0].totalMarks : 'Not Taken' }}</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Technical MCQ Test QP Set</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().quizzes?.[1]?.title || '—' }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Technical MCQ Test Score</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().quizzes?.[1]?.completed ? selectedMember().quizzes[1].score + '/' + selectedMember().quizzes[1].totalMarks : 'Not Taken' }}</td>
</tr>
</table>
<!-- Render evaluations -->
@for (ev of selectedMember().evaluations; track ev._id) {
<div class="print-eval-block" style="margin-bottom: 30px; page-break-inside: avoid;">
<div class="print-eval-title" style="font-weight: bold; margin-bottom: 10px;">
{{ ev.evaluatorRole === 'hr' ? 'HR' : ev.evaluatorRole === 'pm' ? 'Project Manager' : 'Interviewer' }}'s Comments ({{ ev.evaluatorId?.name }}):
</div>
<div class="print-comments" style="min-height: 80px; margin-bottom: 15px;">
{{ ev.comments || 'No comments provided.' }}
</div>
<div class="print-recommendation" style="margin-bottom: 20px;">
<strong style="margin-right: 15px;">Recommendation:</strong>
<span style="margin-right: 15px;"><span style="font-size: 16px;">{{ ev.recommendation === 'offer' ? '☑' : '☐' }}</span> Offer/Hire as Intern</span>
<span style="margin-right: 15px;"><span style="font-size: 16px;">{{ ev.recommendation === 'on_hold' ? '☑' : '☐' }}</span> On Hold</span>
<span style="margin-right: 15px;"><span style="font-size: 16px;">{{ ev.recommendation === 'rejected' ? '☑' : '☐' }}</span> Rejected</span>
<span><span style="font-size: 16px;">{{ ev.recommendation === '2nd_round' ? '☑' : '☐' }}</span> 2nd Round</span>
</div>
<div class="print-signature-row" style="display: flex; justify-content: space-between; align-items: flex-end;">
<div class="print-signature" style="display: flex; align-items: flex-end;">
<strong>Evaluator's Signature:</strong>
@if (ev.evaluatorId?.signature) {
<img [src]="'http://localhost:5000' + ev.evaluatorId.signature" style="max-height: 40px; margin-left: 10px;">
} @else {
<span style="border-bottom: 1px solid #000; display: inline-block; width: 150px; margin-left: 10px;"></span>
}
</div>
<div class="print-date" style="display: flex; align-items: flex-end;">
<strong>Date:</strong>
<span style="border-bottom: 1px solid #000; display: inline-block; width: 120px; margin-left: 10px; text-align: center;">{{ ev.date | date:'shortDate' }}</span>
</div>
</div>
</div>
}
</div>
}
...@@ -74,11 +74,16 @@ export class GroupInterviewComponent implements OnInit { ...@@ -74,11 +74,16 @@ export class GroupInterviewComponent implements OnInit {
pendingSetsCount = 2; pendingSetsCount = 2;
quizSets: QuizSet[] = []; quizSets: QuizSet[] = [];
// ── Detail modal state ──────────────────────────────────── // ── Group detail modal state ─────────────────────────────
showDetailModal = signal(false); showDetailModal = signal(false);
selectedGroup = signal<any>(null); selectedGroup = signal<any>(null);
evalComment = signal('');
evalRecommendation = signal(''); // ── Per-candidate (member) detail modal ──────────────────
showMemberDetailModal = signal(false);
selectedMember = signal<any>(null); // full interview doc
memberEvalComment = '';
memberEvalRecommendation = '';
isMemberSubmitting = signal(false);
constructor(private quizService: QuizService, public authService: AuthService) {} constructor(private quizService: QuizService, public authService: AuthService) {}
...@@ -244,7 +249,7 @@ export class GroupInterviewComponent implements OnInit { ...@@ -244,7 +249,7 @@ export class GroupInterviewComponent implements OnInit {
}); });
} }
// ── Detail modal ────────────────────────────────────────── // ── Group detail modal ────────────────────────────────────
openDetail(group: any): void { openDetail(group: any): void {
this.selectedGroup.set(group); this.selectedGroup.set(group);
this.showDetailModal.set(true); this.showDetailModal.set(true);
...@@ -255,6 +260,108 @@ export class GroupInterviewComponent implements OnInit { ...@@ -255,6 +260,108 @@ export class GroupInterviewComponent implements OnInit {
this.selectedGroup.set(null); this.selectedGroup.set(null);
} }
// ── Member (candidate) detail modal ──────────────────────
openMemberDetail(interviewId: string, event: Event): void {
event.stopPropagation();
this.quizService.getInterviewById(interviewId).subscribe({
next: res => {
this.selectedMember.set(res.interview);
this.memberEvalComment = '';
this.memberEvalRecommendation = '';
this.showMemberDetailModal.set(true);
}
});
}
closeMemberDetail(): void {
this.showMemberDetailModal.set(false);
this.selectedMember.set(null);
}
submitMemberEvaluation(): void {
const m = this.selectedMember();
if (!m || !this.memberEvalRecommendation) return;
this.isMemberSubmitting.set(true);
this.quizService.submitEvaluation(m._id, {
comments: this.memberEvalComment,
recommendation: this.memberEvalRecommendation
}).subscribe({
next: res => {
this.selectedMember.set(res.interview);
this.memberEvalComment = '';
this.memberEvalRecommendation = '';
this.isMemberSubmitting.set(false);
// refresh the group list so chips update
this.loadInterviews();
},
error: () => this.isMemberSubmitting.set(false)
});
}
setMemberDecision(decision: string): void {
const m = this.selectedMember();
if (!m) return;
this.quizService.setInterviewDecision(m._id, decision).subscribe({
next: res => {
this.selectedMember.set(res.interview);
this.loadInterviews();
this.loadStats();
}
});
}
hasMemberEvaluated(): boolean {
const m = this.selectedMember();
if (!m) return false;
const userId = this.authService.currentUser()?.id;
return m.evaluations?.some((e: any) => e.evaluatorId?._id === userId || e.evaluatorId === userId);
}
allMemberEvaluationsDone(): boolean {
const m = this.selectedMember();
if (!m) return false;
const total = (m.assignedInterviewers?.length || 0) +
(m.assignedHRs?.length || 0) +
(m.assignedPMs?.length || 0);
if (total === 0) return false;
return (m.evaluations?.length || 0) >= total;
}
/** True when THIS user still needs to evaluate this member */
needsEvaluation(m: any): boolean {
if (m.status === 'completed') return false;
const userId = this.authService.currentUser()?.id;
const role = this.authService.getUserRole();
if (role === 'admin') return false; // admin gives final decision, not evaluation
const evaluated = m.evaluations?.some(
(e: any) => e.evaluatorId?._id === userId || e.evaluatorId === userId
);
return !evaluated;
}
/** Whether ANY assigned staff member hasn't evaluated a given candidate interview yet */
hasPendingEvaluations(m: any): boolean {
if (m.status === 'completed') return false;
const total = (m.assignedInterviewers?.length || 0) +
(m.assignedHRs?.length || 0) +
(m.assignedPMs?.length || 0);
return (m.evaluations?.length || 0) < total;
}
isPdfGenerating = signal(false);
downloadEvaluationPdf(): void {
const m = this.selectedMember();
if (!m) return;
this.isPdfGenerating.set(true);
setTimeout(() => {
window.print();
this.isPdfGenerating.set(false);
}, 500);
}
// ── Helpers ─────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────
getStatusClass(status: string): string { getStatusClass(status: string): string {
const map: any = { const map: any = {
......
...@@ -370,8 +370,8 @@ ...@@ -370,8 +370,8 @@
} }
</div> </div>
<!-- Final Decision (admin only) --> <!-- Final Decision (admin and hr only) -->
@if (authService.getUserRole() === 'admin' && selectedInterview().status !== 'completed') { @if ((authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') && selectedInterview().status !== 'completed') {
<div class="detail-section decision-section"> <div class="detail-section decision-section">
<h3 class="detail-section-title">Final Decision</h3> <h3 class="detail-section-title">Final Decision</h3>
<div class="decision-buttons"> <div class="decision-buttons">
...@@ -393,7 +393,7 @@ ...@@ -393,7 +393,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@if (authService.getUserRole() === 'admin') { @if (authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') {
<button class="btn btn-danger btn-sm" (click)="deleteInterview(selectedInterview()._id)">Delete Interview</button> <button class="btn btn-danger btn-sm" (click)="deleteInterview(selectedInterview()._id)">Delete Interview</button>
} }
<button class="btn btn-outline" (click)="closeDetail()">Close</button> <button class="btn btn-outline" (click)="closeDetail()">Close</button>
......
...@@ -33,8 +33,8 @@ ...@@ -33,8 +33,8 @@
<div class="user-card-info"> <div class="user-card-info">
<div class="name-row"> <div class="name-row">
<h4>{{ user.name }}</h4> <h4>{{ user.name }}</h4>
<span class="level-badge" [attr.data-level]="user.level || 'beginner'"> <span class="level-badge" [attr.data-level]="user.level || 'Fresher'">
{{ (user.level || 'beginner') | titlecase }} {{ (user.level || 'Fresher') | titlecase }}
</span> </span>
</div> </div>
<p>{{ user.email }}</p> <p>{{ user.email }}</p>
......
/* ═══════════════════════════════════════════════════════
PAGE LAYOUT
═══════════════════════════════════════════════════════ */
.page-container { padding: 32px 40px; }
.content-wrapper { max-width: 1200px; margin: 0; }
.page-header {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px;
}
.page-header h1 { font-size: 24px; font-weight: 700; color: var(--text-primary); margin: 0 0 4px; }
.page-subtitle { font-size: 14px; color: var(--text-muted); margin: 0; }
/* Stats */
.stats-row { display: flex; gap: 16px; margin-bottom: 24px; }
.mini-stat { flex: 1; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 12px; padding: 16px 20px; text-align: center; }
.mini-stat-value { display: block; font-size: 28px; font-weight: 700; color: var(--text-primary); }
.mini-stat-value.orange { color: #f59e0b; }
.mini-stat-value.blue { color: #3b82f6; }
.mini-stat-value.green { color: #22c55e; }
.mini-stat-value.red { color: #ef4444; }
.mini-stat-label { font-size: 12px; color: var(--text-muted); font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; }
/* Filter */
.filter-bar { margin-bottom: 20px; }
.filter-select {
padding: 8px 14px; border: 1px solid var(--border-color); border-radius: 8px;
background: var(--bg-input); color: var(--text-primary); font-size: 14px; font-family: inherit;
}
/* ═══════════════════════════════════════════════════════
INTERVIEW CARDS
═══════════════════════════════════════════════════════ */
.interview-list { display: flex; flex-direction: column; gap: 16px; }
.interview-card {
cursor: pointer; transition: all 0.2s; border: 1px solid var(--border-color);
border-radius: 16px; background: var(--bg-card);
}
.interview-card:hover { border-color: var(--accent-primary); box-shadow: 0 4px 20px rgba(102,126,234,0.12); transform: translateY(-1px); }
.iv-card-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.iv-group-info { display: flex; align-items: center; gap: 14px; }
.iv-group-avatar {
width: 48px; height: 48px; border-radius: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.iv-group-avatar .material-symbols-rounded { color: #fff; font-size: 26px; }
.iv-name { font-size: 16px; font-weight: 700; color: var(--text-primary); margin: 0; }
.iv-email { font-size: 13px; color: var(--text-muted); margin: 0; }
.iv-badges { display: flex; gap: 8px; flex-wrap: wrap; }
.iv-card-meta {
display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 14px;
padding-bottom: 12px; border-bottom: 1px solid var(--border-color);
}
.meta-item { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary); }
.meta-item .material-symbols-rounded { font-size: 16px; color: var(--text-muted); }
/* Candidate chips row */
.candidate-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.candidate-chip {
padding: 4px 12px; border-radius: 16px; display: flex; align-items: center;
justify-content: center; font-size: 12px; font-weight: 600; color: #fff!important;
border: 2px solid var(--bg-card); background: #667eea;
transition: transform 0.15s;
white-space: nowrap;
}
.candidate-chip:hover { transform: scale(1.15); z-index: 1; }
.candidate-chip.clickable-chip { cursor: pointer; }
.candidate-chip.clickable-chip:hover { transform: scale(1.12); box-shadow: 0 4px 12px rgba(0,0,0,0.18); }
.candidate-chip.badge-warning { background: #f59e0b; }
.candidate-chip.badge-info { background: #3b82f6; }
.candidate-chip.badge-success { background: #22c55e; }
.candidate-chip.badge-purple { background: #a855f7; }
.iv-card-bottom { display: flex; justify-content: flex-end; }
.status-summary { font-size: 12px; color: var(--text-muted); font-style: italic; }
/* ⚠ badge on chip */
.chip-alert { font-size: 11px; margin-right: 3px; }
/* Evaluation pending badge inside candidate row name */
.eval-pending-badge {
display: inline-flex; align-items: center; font-size: 11px; font-weight: 600;
padding: 2px 7px; border-radius: 10px; margin-left: 8px;
background: rgba(245,158,11,0.15); color: #f59e0b;
}
.eval-pending-badge.warn { background: rgba(102,126,234,0.1); color: #667eea; }
/* Clickable candidate left section (button reset) */
.cr-clickable {
display: flex; align-items: center; gap: 12px;
background: none; border: none; cursor: pointer; text-align: left;
padding: 6px 10px; border-radius: 10px; transition: background 0.15s;
min-width: 160px; flex-shrink: 0;
}
.cr-clickable:hover { background: rgba(102,126,234,0.08); }
/* Hint text at end of bottom row */
.cr-open-hint { font-size: 11px; color: var(--text-muted); margin-left: auto; font-style: italic; }
/* ═══════════════════════════════════════════════════════
BADGES
═══════════════════════════════════════════════════════ */
.badge { padding: 4px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.badge-warning { background: rgba(245,158,11,0.1); color: #f59e0b; }
.badge-info { background: rgba(59,130,246,0.1); color: #3b82f6; }
.badge-purple { background: rgba(168,85,247,0.1); color: #a855f7; }
.badge-success { background: rgba(34,197,94,0.1); color: #22c55e; }
.badge-danger { background: rgba(239,68,68,0.1); color: #ef4444; }
.badge-muted { background: var(--bg-hover); color: var(--text-muted); }
.badge-group { background: linear-gradient(135deg,rgba(102,126,234,.15),rgba(118,75,162,.15)); color: #667eea; }
/* ═══════════════════════════════════════════════════════
EMPTY / LOADING
═══════════════════════════════════════════════════════ */
.loading-center { display: flex; flex-direction: column; align-items: center; padding: 80px; gap: 16px; color: var(--text-muted); }
.empty-state { text-align: center; padding: 80px; color: var(--text-muted); }
.empty-state .material-symbols-rounded { font-size: 64px; display: block; margin-bottom: 16px; opacity: 0.35; }
.empty-state h3 { color: var(--text-primary); font-size: 18px; margin: 0 0 8px; }
.empty-state p { font-size: 14px; margin: 0; }
/* ═══════════════════════════════════════════════════════
MODAL
═══════════════════════════════════════════════════════ */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.5); backdrop-filter: blur(5px);
z-index: 1000; display: flex; align-items: center; justify-content: center;
animation: fadeIn 0.2s ease-out;
}
.modal-container {
background: var(--bg-card); border-radius: 18px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3); border: 1px solid var(--border-color);
width: 92%; max-height: 88vh; overflow-y: auto;
animation: slideUp 0.3s cubic-bezier(0.16,1,0.3,1);
}
.modal-xl { max-width: 860px; }
.modal-header {
padding: 22px 26px; border-bottom: 1px solid var(--border-color);
display: flex; justify-content: space-between; align-items: center;
position: sticky; top: 0; background: var(--bg-card); z-index: 2; border-radius: 18px 18px 0 0;
}
.modal-header h2 { font-size: 18px; font-weight: 700; margin: 0; }
.modal-body { padding: 26px; display: flex; flex-direction: column; gap: 8px; }
.modal-footer {
padding: 16px 26px; border-top: 1px solid var(--border-color);
display: flex; justify-content: flex-end; gap: 12px;
background: var(--bg-hover); position: sticky; bottom: 0; border-radius: 0 0 18px 18px;
}
/* Success banner */
.success-banner {
margin: 24px; padding: 20px 24px; border-radius: 14px;
background: linear-gradient(135deg, rgba(34,197,94,.12), rgba(16,185,129,.08));
border: 1px solid rgba(34,197,94,.3); color: #22c55e;
display: flex; align-items: center; gap: 12px; font-size: 15px; font-weight: 500;
}
.success-banner .material-symbols-rounded { font-size: 28px; }
.success-banner .btn { margin-left: auto; }
/* ═══════════════════════════════════════════════════════
FORM ELEMENTS
═══════════════════════════════════════════════════════ */
.section-title {
display: flex; align-items: center; gap: 8px;
font-size: 13px; font-weight: 700; color: var(--text-primary);
text-transform: uppercase; letter-spacing: 0.8px;
padding: 10px 0 6px; border-bottom: 1px solid var(--border-color); margin-bottom: 14px; margin-top: 10px;
}
.section-title .material-symbols-rounded { font-size: 18px; color: var(--accent-primary); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 14px; }
.form-row.three-col { grid-template-columns: 1fr 1fr 1fr; }
.form-group { display: flex; flex-direction: column; gap: 6px; }
.form-label { font-size: 13px; font-weight: 600; color: var(--text-primary); }
.form-input {
padding: 10px 14px; border: 1px solid var(--border-color); border-radius: 8px;
background: var(--bg-input); color: var(--text-primary); font-size: 14px;
font-family: inherit; transition: all 0.2s; width: 100%; box-sizing: border-box;
}
.form-input:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px rgba(102,126,234,0.12); }
.field-hint {
display: inline-flex; align-items: center; gap: 4px;
font-size: 12px; color: #22c55e; margin-top: 2px;
}
.field-hint .material-symbols-rounded { font-size: 14px; }
/* Checklist box */
.checklist-box {
border: 1px solid var(--border-color); border-radius: 10px; padding: 10px 12px;
max-height: 130px; overflow-y: auto; background: var(--bg-input);
display: flex; flex-direction: column; gap: 6px;
}
.check-item {
display: flex; align-items: center; gap: 8px; font-size: 13px;
color: var(--text-secondary); cursor: pointer; padding: 2px 0;
}
.check-item input[type="checkbox"] { accent-color: var(--accent-primary); }
.check-item:hover { color: var(--text-primary); }
.text-muted { color: var(--text-muted); font-size: 13px; }
/* ═══════════════════════════════════════════════════════
QUIZ CONFIGURATION
═══════════════════════════════════════════════════════ */
.quiz-empty-hint {
display: flex; flex-direction: column; align-items: center; gap: 10px;
padding: 32px; border: 2px dashed var(--border-color); border-radius: 14px;
background: var(--bg-hover); color: var(--text-muted); margin-bottom: 8px;
}
.quiz-empty-hint .material-symbols-rounded { font-size: 40px; opacity: 0.4; }
.quiz-empty-hint p { margin: 0; font-size: 14px; }
.quiz-setup-prompt {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
padding: 14px 18px; border-radius: 10px;
background: rgba(102,126,234,0.06); border: 1px solid rgba(102,126,234,0.2);
margin-bottom: 16px;
}
.quiz-setup-prompt .material-symbols-rounded { font-size: 20px; color: var(--accent-primary); }
.quiz-setup-prompt span { font-size: 14px; font-weight: 500; color: var(--text-primary); }
.sets-count-input {
width: 72px; padding: 8px 10px; border: 1px solid var(--border-color); border-radius: 8px;
background: var(--bg-input); color: var(--text-primary); font-size: 14px;
text-align: center; font-family: inherit;
}
.sets-count-input:focus { outline: none; border-color: var(--accent-primary); }
/* Quiz Set Block */
.quiz-set-block {
border: 1px solid var(--border-color); border-radius: 14px;
padding: 18px 20px; margin-bottom: 14px;
background: var(--bg-hover); display: flex; flex-direction: column; gap: 10px;
transition: border-color 0.2s;
}
.quiz-set-block:hover { border-color: rgba(102,126,234,0.4); }
.quiz-set-header { display: flex; align-items: center; justify-content: space-between; }
.quiz-set-title { display: flex; align-items: center; gap: 10px; }
.set-badge {
background: linear-gradient(135deg, #667eea, #764ba2); color: #fff;
font-size: 12px; font-weight: 700; padding: 4px 12px; border-radius: 20px;
}
.set-note { font-size: 12px; color: var(--text-muted); }
/* Quiz entry row */
.quiz-entry-row {
display: flex; align-items: center; gap: 10px;
}
.quiz-entry-row .form-input { flex: 1; }
/* Assignment mode */
.assignment-mode-row {
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
padding-top: 8px; border-top: 1px solid var(--border-color);
}
.mode-label { font-size: 13px; font-weight: 600; color: var(--text-primary); }
.mode-toggle { display: flex; gap: 8px; }
.mode-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 7px 16px; border-radius: 8px; font-size: 13px; font-weight: 500;
border: 1px solid var(--border-color); background: var(--bg-input);
color: var(--text-secondary); cursor: pointer; transition: all 0.2s;
}
.mode-btn .material-symbols-rounded { font-size: 16px; }
.mode-btn:hover { border-color: var(--accent-primary); color: var(--text-primary); }
.mode-btn.active { border-color: #667eea; background: rgba(102,126,234,0.12); color: #667eea; font-weight: 700; }
/* Direct assignment table */
.direct-assignment-table {
border: 1px solid var(--border-color); border-radius: 10px; overflow: hidden;
margin-top: 4px;
}
.da-header {
display: grid; grid-template-columns: 1fr 1.5fr;
padding: 10px 14px; background: rgba(102,126,234,0.08);
font-size: 12px; font-weight: 700; color: var(--text-primary); text-transform: uppercase; letter-spacing: 0.5px;
}
.da-row {
display: grid; grid-template-columns: 1fr 1.5fr; align-items: center;
padding: 10px 14px; border-top: 1px solid var(--border-color);
gap: 12px; background: var(--bg-card); transition: background 0.15s;
}
.da-row:hover { background: var(--bg-hover); }
.da-candidate { display: flex; align-items: center; gap: 10px; }
.da-avatar {
width: 30px; height: 30px; border-radius: 8px; font-size: 13px; font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2); color: #fff;
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.da-row .form-input { padding: 7px 10px; font-size: 13px; }
/* Ghost btn */
.btn-ghost {
background: none; border: 1px dashed var(--border-color); color: var(--text-muted);
padding: 7px 14px; border-radius: 8px; font-size: 13px; cursor: pointer;
display: inline-flex; align-items: center; gap: 6px; transition: all 0.2s;
font-family: inherit;
}
.btn-ghost:hover { border-color: var(--accent-primary); color: var(--accent-primary); }
.btn-ghost .material-symbols-rounded { font-size: 16px; }
.icon-btn.danger { color: #ef4444; }
.icon-btn.danger:hover { background: rgba(239,68,68,0.08); }
/* ═══════════════════════════════════════════════════════
DETAIL MODAL
═══════════════════════════════════════════════════════ */
.detail-body { display: flex; flex-direction: column; gap: 24px; }
.detail-section { padding-bottom: 16px; border-bottom: 1px solid var(--border-color); }
.detail-section:last-child { border-bottom: none; }
.detail-section-title { font-size: 12px; font-weight: 700; color: var(--text-primary); margin: 0 0 16px; text-transform: uppercase; letter-spacing: 0.8px; }
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.detail-item { display: flex; flex-direction: column; gap: 3px; }
.detail-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
.detail-value { font-size: 14px; color: var(--text-primary); font-weight: 500; }
/* Candidate detail list */
.candidate-detail-list { display: flex; flex-direction: column; gap: 12px; }
.candidate-row {
display: flex; flex-direction: column; gap: 12px; padding: 16px;
border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-hover);
transition: border-color 0.2s;
}
.candidate-row:hover { border-color: rgba(102,126,234,0.35); }
/* Top row: avatar+name on left, progress stepper fills the rest */
.candidate-row-top { display: flex; align-items: center; gap: 16px; }
.candidate-row-left { display: flex; align-items: center; gap: 12px; min-width: 160px; flex-shrink: 0; }
.iv-avatar { width: 40px; height: 40px; border-radius: 10px; background: linear-gradient(135deg,#667eea,#764ba2); color:#fff; font-weight:700; font-size:16px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.iv-avatar.small { width: 36px; height: 36px; font-size: 15px; border-radius: 9px; }
.cr-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.cr-email { font-size: 12px; color: var(--text-muted); }
.candidate-row-mid { flex: 1; }
/* Bottom row: status badge + quiz chips + accept/reject — all left-aligned */
.candidate-row-bottom {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
padding-top: 10px; border-top: 1px solid var(--border-color);
}
/* Progress steps (mini) */
.progress-steps { display: flex; align-items: center; }
.progress-steps.mini .step-label { font-size: 10px; }
.progress-steps.mini .step-dot { width: 8px; height: 8px; }
.step { display: flex; align-items: center; gap: 4px; }
.step-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--border-color); transition: all 0.3s; }
.step.done .step-dot { background: #22c55e; }
.step.active .step-dot { background: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,0.2); }
.step-label { font-size: 11px; color: var(--text-muted); font-weight: 500; }
.step.done .step-label { color: #22c55e; }
.step.active .step-label { color: #667eea; font-weight: 600; }
.step-line { flex: 1; height: 2px; background: var(--border-color); margin: 0 6px; transition: background 0.3s; min-width: 16px; }
.step-line.done { background: #22c55e; }
/* Quiz score chips */
.quiz-scores-inline { display: flex; flex-wrap: wrap; gap: 6px; }
.quiz-score-chip {
font-size: 11px; padding: 3px 10px; border-radius: 20px;
background: var(--bg-card); border: 1px solid var(--border-color);
color: var(--text-muted); white-space: nowrap;
}
.quiz-score-chip.completed { color: #22c55e; border-color: rgba(34,197,94,0.3); background: rgba(34,197,94,0.07); }
/* Mini decision buttons */
.mini-decision-btns { display: flex; gap: 6px; }
.mini-btn {
padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600;
border: none; cursor: pointer; transition: opacity 0.2s; font-family: inherit;
}
.mini-btn.success { background: rgba(34,197,94,0.15); color: #22c55e; }
.mini-btn.success:hover { background: #22c55e; color: #fff; }
.mini-btn.danger { background: rgba(239,68,68,0.12); color: #ef4444; }
.mini-btn.danger:hover { background: #ef4444; color: #fff; }
/* ═══════════════════════════════════════════════════════
BUTTONS (shared)
═══════════════════════════════════════════════════════ */
.btn-success { background: #22c55e; color: #fff; }
.btn-danger { background: #ef4444; color: #fff; }
.btn-sm { padding: 6px 14px; font-size: 13px; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
/* ═══════════════════════════════════════════════════════
EVALUATION PANEL (member detail modal)
Mirrors individual-interview styles
═══════════════════════════════════════════════════════ */
.quiz-results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.quiz-result-card {
padding: 14px 16px; border-radius: 12px; border: 1px solid var(--border-color);
background: var(--bg-input);
}
.qr-title { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; }
.qr-score { font-size: 22px; font-weight: 700; color: #22c55e; }
.qr-pending { font-size: 13px; color: var(--text-muted); font-style: italic; }
.eval-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }
.eval-card {
padding: 14px 16px; border-radius: 12px; border: 1px solid var(--border-color);
background: var(--bg-input);
}
.eval-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.eval-evaluator { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.eval-role { font-size: 10px !important; padding: 2px 6px !important; }
.eval-comments { font-size: 13px; color: var(--text-secondary); font-style: italic; margin: 6px 0 8px; }
.eval-date { font-size: 11px; color: var(--text-muted); }
.eval-form {
margin-top: 16px; padding: 18px 20px; border-radius: 14px;
border: 1px dashed rgba(102,126,234,0.35);
background: rgba(102,126,234,0.04);
display: flex; flex-direction: column; gap: 12px;
}
.eval-form h4 { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; }
.form-textarea { resize: vertical; min-height: 80px; }
.decision-section { border-bottom: none; }
.decision-buttons { display: flex; flex-wrap: wrap; gap: 10px; }
.btn-success { background: #22c55e; color: #fff; border: none; }
.btn-warning { background: #f59e0b; color: #fff; border: none; }
/* ═══════════════════════════════════════════════════════
RESPONSIVE
═══════════════════════════════════════════════════════ */
@media (max-width: 768px) {
.page-container { padding: 20px; }
.stats-row { flex-wrap: wrap; }
.mini-stat { min-width: 100px; }
.form-row, .form-row.three-col { grid-template-columns: 1fr; }
.detail-grid { grid-template-columns: 1fr; }
.candidate-row { flex-direction: column; }
.candidate-row-right { align-items: flex-start; }
.da-header, .da-row { grid-template-columns: 1fr; }
}
/* ═══════════════════════════════════════════════════════
PRINT STYLES FOR EVALUATION PDF
═══════════════════════════════════════════════════════ */
.print-container {
display: none;
}
@media print {
body * {
visibility: hidden;
}
.print-container, .print-container * {
visibility: visible;
}
.print-container {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
padding: 20px;
background: white;
color: black;
font-family: Arial, sans-serif;
}
.print-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #0078d4;
padding-bottom: 10px;
margin-bottom: 20px;
}
}
<p>group-interview works!</p> <div class="page-container animate-fade-in">
<div class="content-wrapper">
<!-- Page Header -->
<div class="page-header">
<div>
<h1>Group Interviews</h1>
<p class="page-subtitle">Create and manage batch interview sessions for candidate groups</p>
</div>
<button class="btn btn-primary" (click)="openCreateModal()">
<span class="material-symbols-rounded">group_add</span>
Create Group Interview
</button>
</div>
<!-- Stats -->
<div class="stats-row">
<div class="mini-stat">
<span class="mini-stat-value">{{ stats().total }}</span>
<span class="mini-stat-label">Total</span>
</div>
<div class="mini-stat">
<span class="mini-stat-value orange">{{ stats().pending }}</span>
<span class="mini-stat-label">In Progress</span>
</div>
<div class="mini-stat">
<span class="mini-stat-value blue">{{ stats().completed }}</span>
<span class="mini-stat-label">Completed</span>
</div>
<div class="mini-stat">
<span class="mini-stat-value green">{{ stats().accepted }}</span>
<span class="mini-stat-label">Accepted</span>
</div>
<div class="mini-stat">
<span class="mini-stat-value red">{{ stats().rejected }}</span>
<span class="mini-stat-label">Rejected</span>
</div>
</div>
<!-- Filter -->
<div class="filter-bar">
<select class="filter-select" [(ngModel)]="filterStatus" (ngModelChange)="onFilterChange()">
<option value="">All Statuses</option>
<option value="pending">Pending</option>
<option value="quiz_phase">Quiz Phase</option>
<option value="coding_phase">Coding Phase</option>
<option value="evaluation">Evaluation</option>
<option value="completed">Completed</option>
</select>
</div>
<!-- Interview Group List -->
@if (loading()) {
<div class="loading-center"><div class="spinner spinner-lg"></div><p>Loading interviews...</p></div>
} @else if (groupedList().length === 0) {
<div class="empty-state">
<span class="material-symbols-rounded">groups</span>
<h3>No group interviews found</h3>
<p>Create your first group interview session to get started</p>
</div>
} @else {
<div class="interview-list">
@for (g of groupedList(); track g.groupId) {
<div class="interview-card card card-padding" (click)="openDetail(g)">
<div class="iv-card-top">
<div class="iv-group-info">
<div class="iv-group-avatar">
<span class="material-symbols-rounded">groups</span>
</div>
<div>
<h3 class="iv-name">{{ g.groupId }}</h3>
<p class="iv-email">{{ g.members.length }} candidate{{ g.members.length !== 1 ? 's' : '' }} · {{ g.position }}</p>
</div>
</div>
<div class="iv-badges">
<span class="badge badge-group">Group</span>
@if (completedCount(g.members) === g.members.length && g.members.length > 0) {
<span class="badge badge-success">All Done</span>
} @else {
<span class="badge badge-warning">In Progress</span>
}
</div>
</div>
<div class="iv-card-meta">
<span class="meta-item"><span class="material-symbols-rounded">work</span> {{ g.position }}</span>
@if (g.techStack) {
<span class="meta-item"><span class="material-symbols-rounded">code</span> {{ g.techStack }}</span>
}
<span class="meta-item"><span class="material-symbols-rounded">calendar_today</span> {{ g.dateOfInterview | date:'mediumDate' }}</span>
@if (g.assignedInterviewers?.length > 0) {
<span class="meta-item"><span class="material-symbols-rounded">person</span> {{ g.assignedInterviewers[0]?.name }}</span>
}
</div>
<!-- Candidate progress chips — click any chip to open the evaluation panel directly -->
<div class="candidate-chips">
@for (m of g.members; track m._id) {
<span class="candidate-chip clickable-chip" [ngClass]="getStatusClass(m.status)"
[title]="m.candidateId?.name + ' — ' + formatStatus(m.status) + (hasPendingEvaluations(m) ? ' (Evaluations pending)' : '')"
(click)="openMemberDetail(m._id, $event)">
@if (hasPendingEvaluations(m)) {
<span class="chip-alert" title="Evaluations still pending">⚠️</span>
}
{{ m.candidateId?.name }}
</span>
}
</div>
<div class="iv-card-bottom">
<span class="status-summary">{{ groupStatusSummary(g.members) }}</span>
</div>
</div>
}
</div>
}
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════
CREATE GROUP INTERVIEW MODAL
═══════════════════════════════════════════════════════════ -->
@if (showCreateModal()) {
<div class="modal-overlay" (click)="closeCreateModal()">
<div class="modal-container modal-xl" (click)="$event.stopPropagation()">
<div class="modal-header">
<h2>Create Group Interview</h2>
<button class="icon-btn" (click)="closeCreateModal()"><span class="material-symbols-rounded">close</span></button>
</div>
@if (successMsg()) {
<div class="success-banner">
<span class="material-symbols-rounded">check_circle</span>
{{ successMsg() }}
<button class="btn btn-outline btn-sm" (click)="closeCreateModal()">Close</button>
</div>
} @else {
<div class="modal-body">
<!-- Staff assignment -->
<div class="section-title">
<span class="material-symbols-rounded">badge</span> Staff Assignment
</div>
<div class="form-row three-col">
<div class="form-group">
<label class="form-label">Interviewers</label>
<div class="checklist-box">
@for (i of interviewers(); track i._id) {
<label class="check-item">
<input type="checkbox" [value]="i._id"
(change)="toggleSelection($event, newGroupInterview.assignedInterviewers)">
<span>{{ i.name }}</span>
</label>
}
@if (interviewers().length === 0) { <p class="text-muted">No interviewers found</p> }
</div>
</div>
<div class="form-group">
<label class="form-label">HR Personnel</label>
<div class="checklist-box">
@for (h of hrs(); track h._id) {
<label class="check-item">
<input type="checkbox" [value]="h._id"
(change)="toggleSelection($event, newGroupInterview.assignedHRs)">
<span>{{ h.name }}</span>
</label>
}
@if (hrs().length === 0) { <p class="text-muted">No HR found</p> }
</div>
</div>
<div class="form-group">
<label class="form-label">Project Managers</label>
<div class="checklist-box">
@for (p of pms(); track p._id) {
<label class="check-item">
<input type="checkbox" [value]="p._id"
(change)="toggleSelection($event, newGroupInterview.assignedPMs)">
<span>{{ p.name }}</span>
</label>
}
@if (pms().length === 0) { <p class="text-muted">No PMs found</p> }
</div>
</div>
</div>
<!-- Group + Position -->
<div class="section-title">
<span class="material-symbols-rounded">groups</span> Group & Position
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Group *</label>
<select class="form-input" [(ngModel)]="newGroupInterview.groupName" (ngModelChange)="onGroupChange()">
<option value="">Select group</option>
@for (g of groups(); track g) {
<option [value]="g">{{ g }}</option>
}
</select>
@if (groupMembers().length > 0) {
<span class="field-hint">
<span class="material-symbols-rounded">person</span>
{{ groupMembers().length }} candidate{{ groupMembers().length !== 1 ? 's' : '' }} in this group
</span>
}
</div>
<div class="form-group">
<label class="form-label">Position *</label>
<input class="form-input" [(ngModel)]="newGroupInterview.position" placeholder="e.g., MEAN Stack Developer">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Source</label>
<input class="form-input" [(ngModel)]="newGroupInterview.source" placeholder="e.g., LinkedIn, Campus">
</div>
<div class="form-group">
<label class="form-label">Date of Interview</label>
<input class="form-input" type="date" [(ngModel)]="newGroupInterview.dateOfInterview">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Tech Stack</label>
<input class="form-input" [(ngModel)]="newGroupInterview.techStack" placeholder="e.g., MERN, Java, Python">
</div>
</div>
<!-- Quiz Configuration -->
<div class="section-title">
<span class="material-symbols-rounded">quiz</span> Quiz Configuration
@if (quizSets.length > 0) {
<button class="btn btn-outline btn-sm" style="margin-left: auto;" (click)="addMoreSet()">
<span class="material-symbols-rounded">add</span> Add Set
</button>
}
</div>
@if (quizSets.length === 0 && !showQuizSetup) {
<div class="quiz-empty-hint">
<span class="material-symbols-rounded">quiz</span>
<p>No quiz sets configured yet.</p>
<button class="btn btn-primary" (click)="showQuizSetup = true">
<span class="material-symbols-rounded">add</span> Add Quiz
</button>
</div>
}
@if (showQuizSetup) {
<div class="quiz-setup-prompt">
<span class="material-symbols-rounded">help_outline</span>
<span>How many quiz sets?</span>
<input type="number" min="1" max="10" class="sets-count-input"
[(ngModel)]="pendingSetsCount" placeholder="e.g. 2">
<button class="btn btn-primary btn-sm" (click)="confirmSetsCount()">Confirm</button>
<button class="btn btn-outline btn-sm" (click)="showQuizSetup = false">Cancel</button>
</div>
}
<!-- Quiz Sets -->
@for (set of quizSets; track $index; let si = $index) {
<div class="quiz-set-block">
<div class="quiz-set-header">
<div class="quiz-set-title">
<span class="set-badge">Set {{ si + 1 }}</span>
@if (isSingleQuizSet(set)) {
<span class="set-note">📌 Assigned to all candidates</span>
} @else if (isMultiQuizSet(set)) {
<span class="set-note">🎯 {{ validEntries(set).length }} quizzes · {{ set.mode === 'random' ? '🎲 Random' : 'Direct' }} assignment</span>
}
</div>
<button class="icon-btn danger" (click)="removeQuizSet(si)">
<span class="material-symbols-rounded">delete</span>
</button>
</div>
<!-- Quiz entries in this set -->
@for (entry of set.quizEntries; track $index; let ei = $index) {
<div class="quiz-entry-row">
<select class="form-input" [(ngModel)]="entry.quizId">
<option value="">— Select quiz —</option>
@for (q of getAvailableQuizzes(si, ei); track q._id) {
<option [value]="q._id">{{ q.title }}</option>
}
</select>
<button class="icon-btn" (click)="removeQuizEntry(si, ei)">
<span class="material-symbols-rounded">remove_circle_outline</span>
</button>
</div>
}
<button class="btn btn-ghost btn-sm" (click)="addQuizToSet(si)">
<span class="material-symbols-rounded">add</span> Add quiz to Set {{ si + 1 }}
</button>
<!-- Assignment mode — only shown when multiple quizzes -->
@if (isMultiQuizSet(set)) {
<div class="assignment-mode-row">
<span class="mode-label">Assignment Mode:</span>
<div class="mode-toggle">
<button class="mode-btn" [class.active]="set.mode === 'random'" (click)="set.mode = 'random'">
<span class="material-symbols-rounded">shuffle</span> Random
</button>
<button class="mode-btn" [class.active]="set.mode === 'direct'" (click)="set.mode = 'direct'">
<span class="material-symbols-rounded">assignment_ind</span> Direct
</button>
</div>
</div>
<!-- Direct assignment table -->
@if (set.mode === 'direct') {
@if (groupMembers().length === 0) {
<p class="text-muted" style="font-size:13px; margin-top:8px;">
⚠️ Select a group above to assign quizzes to individual candidates.
</p>
} @else {
<div class="direct-assignment-table">
<div class="da-header">
<span>Candidate</span>
<span>Assigned Quiz (Set {{ si + 1 }})</span>
</div>
@for (member of groupMembers(); track member._id) {
<div class="da-row">
<div class="da-candidate">
<div class="da-avatar">{{ member.name.charAt(0).toUpperCase() }}</div>
<span>{{ member.name }}</span>
</div>
<select class="form-input" [(ngModel)]="set.directAssignments[member._id]">
<option value="">— Select —</option>
@for (entry of validEntries(set); track entry.quizId) {
<option [value]="entry.quizId">{{ getQuizTitle(entry.quizId) }}</option>
}
</select>
</div>
}
</div>
}
}
}
</div>
}
</div><!-- /modal-body -->
<div class="modal-footer">
<button class="btn btn-outline" (click)="closeCreateModal()">Cancel</button>
<button class="btn btn-primary"
(click)="createGroupInterview()"
[disabled]="isSubmitting() || !newGroupInterview.groupName || !newGroupInterview.position">
@if (isSubmitting()) {
<span class="spinner spinner-sm"></span> Creating...
} @else {
<span class="material-symbols-rounded">rocket_launch</span> Create Group Interview
}
</button>
</div>
}
</div>
</div>
}
<!-- ═══════════════════════════════════════════════════════════
DETAIL MODAL — All candidates in a group session
═══════════════════════════════════════════════════════════ -->
@if (showDetailModal() && selectedGroup()) {
<div class="modal-overlay" (click)="closeDetail()">
<div class="modal-container modal-xl" (click)="$event.stopPropagation()">
<div class="modal-header">
<div>
<h2>{{ selectedGroup().groupId }}</h2>
<span style="font-size:13px; color:var(--text-muted)">
{{ selectedGroup().position }} · {{ selectedGroup().members.length }} candidates
</span>
</div>
<button class="icon-btn" (click)="closeDetail()"><span class="material-symbols-rounded">close</span></button>
</div>
<div class="modal-body detail-body">
<!-- Session meta -->
<div class="detail-section">
<h3 class="detail-section-title">Session Info</h3>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Position</span>
<span class="detail-value">{{ selectedGroup().position }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Tech Stack</span>
<span class="detail-value">{{ selectedGroup().techStack || '—' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Source</span>
<span class="detail-value">{{ selectedGroup().source || '—' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Interview Date</span>
<span class="detail-value">{{ selectedGroup().dateOfInterview | date:'mediumDate' }}</span>
</div>
</div>
</div>
<!-- Candidate rows -->
<div class="detail-section">
<h3 class="detail-section-title">Candidates</h3>
<div class="candidate-detail-list">
@for (m of selectedGroup().members; track m._id) {
<div class="candidate-row">
<!-- Top row: clickable avatar+name on the left, progress bar on the right -->
<div class="candidate-row-top">
<button class="cr-clickable" (click)="openMemberDetail(m._id, $event)">
<div class="iv-avatar small">{{ m.candidateId?.name?.charAt(0)?.toUpperCase() }}</div>
<div>
<div class="cr-name">
{{ m.candidateId?.name }}
@if (needsEvaluation(m)) {
<span class="eval-pending-badge" title="Your evaluation is pending">⚠️ Evaluate</span>
}
@if (hasPendingEvaluations(m)) {
<span class="eval-pending-badge warn" title="Some evaluations are still pending"></span>
}
</div>
<div class="cr-email">{{ m.candidateId?.email }}</div>
</div>
</button>
<div class="candidate-row-mid">
<div class="progress-steps mini">
<div class="step" [class.done]="['quiz_phase','coding_phase','evaluation','completed'].includes(m.status)">
<span class="step-dot"></span><span class="step-label">Created</span>
</div>
<div class="step-line" [class.done]="['quiz_phase','coding_phase','evaluation','completed'].includes(m.status)"></div>
<div class="step" [class.active]="m.status==='quiz_phase'" [class.done]="['coding_phase','evaluation','completed'].includes(m.status)">
<span class="step-dot"></span><span class="step-label">Quiz</span>
</div>
<div class="step-line" [class.done]="['coding_phase','evaluation','completed'].includes(m.status)"></div>
<div class="step" [class.active]="m.status==='coding_phase'" [class.done]="['evaluation','completed'].includes(m.status)">
<span class="step-dot"></span><span class="step-label">Coding</span>
</div>
<div class="step-line" [class.done]="['evaluation','completed'].includes(m.status)"></div>
<div class="step" [class.active]="m.status==='evaluation'" [class.done]="m.status==='completed'">
<span class="step-dot"></span><span class="step-label">Evaluate</span>
</div>
<div class="step-line" [class.done]="m.status==='completed'"></div>
<div class="step" [class.active]="m.status==='completed'" [class.done]="m.status==='completed'">
<span class="step-dot"></span><span class="step-label">Done</span>
</div>
</div>
</div>
</div>
<!-- Bottom row: status badge + quiz scores (no accept/reject here) -->
<div class="candidate-row-bottom">
<span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span>
@if (m.finalDecision !== 'pending') {
<span class="badge" [ngClass]="getDecisionClass(m.finalDecision)">{{ formatDecision(m.finalDecision) }}</span>
}
@if (m.quizzes?.length > 0) {
@for (q of m.quizzes; track q.quizId) {
<span class="quiz-score-chip" [class.completed]="q.completed">
{{ q.title }}: {{ q.completed ? q.score + '/' + q.totalMarks : 'Pending' }}
</span>
}
}
<span class="cr-open-hint">Click name to evaluate →</span>
</div>
</div>
}
</div>
</div>
</div>
<div class="modal-footer">
@if (authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') {
<button class="btn btn-danger btn-sm" (click)="deleteGroupInterview(selectedGroup().groupId)">
Delete All Interviews
</button>
}
<button class="btn btn-outline" (click)="closeDetail()">Close</button>
</div>
</div>
</div>
}
<!-- ═══════════════════════════════════════════════════════════
MEMBER DETAIL MODAL — full evaluation panel for one candidate
═══════════════════════════════════════════════════════════ -->
@if (showMemberDetailModal() && selectedMember()) {
<div class="modal-overlay" (click)="closeMemberDetail()">
<div class="modal-container modal-xl" (click)="$event.stopPropagation()">
<!-- Header -->
<div class="modal-header">
<div>
<h2>{{ selectedMember().candidateId?.name }}</h2>
<span class="badge" [ngClass]="getStatusClass(selectedMember().status)">{{ formatStatus(selectedMember().status) }}</span>
@if (selectedMember().finalDecision !== 'pending') {
<span class="badge" [ngClass]="getDecisionClass(selectedMember().finalDecision)" style="margin-left:6px">
{{ formatDecision(selectedMember().finalDecision) }}
</span>
}
</div>
<button class="icon-btn" (click)="closeMemberDetail()"><span class="material-symbols-rounded">close</span></button>
</div>
<div class="modal-body detail-body">
<!-- Candidate Info -->
<div class="detail-section">
<h3 class="detail-section-title">Candidate Information</h3>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Name</span>
<span class="detail-value">{{ selectedMember().candidateId?.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Email</span>
<span class="detail-value">{{ selectedMember().candidateId?.email }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Position</span>
<span class="detail-value">{{ selectedMember().position }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Tech Stack</span>
<span class="detail-value">{{ selectedMember().techStack || '—' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Date of Interview</span>
<span class="detail-value">{{ selectedMember().dateOfInterview | date:'mediumDate' }}</span>
</div>
</div>
</div>
<!-- Quiz Results -->
@if (selectedMember().quizzes?.length > 0) {
<div class="detail-section">
<h3 class="detail-section-title">Quiz Results</h3>
<div class="quiz-results-grid">
@for (q of selectedMember().quizzes; track q.quizId) {
<div class="quiz-result-card">
<div class="qr-title">{{ q.title }}</div>
@if (q.completed) {
<div class="qr-score">{{ q.score }}/{{ q.totalMarks }} ({{ q.percentage }}%)</div>
} @else {
<div class="qr-pending">Not Taken</div>
}
</div>
}
</div>
</div>
}
<!-- Evaluations -->
<div class="detail-section">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<h3 class="detail-section-title" style="margin:0;">Evaluations</h3>
<div style="display: flex; gap: 8px; align-items: center;">
@if (allMemberEvaluationsDone()) {
<span class="badge badge-success">All evaluations done</span>
<button class="btn btn-primary btn-sm" (click)="downloadEvaluationPdf()">
@if (isPdfGenerating()) { <span class="spinner spinner-sm"></span> Generating... } @else { <span class="material-symbols-rounded" style="font-size: 16px; margin-right: 4px;">download</span> Download PDF }
</button>
} @else {
<span class="badge badge-warning">Evaluations pending</span>
}
</div>
</div>
<!-- Evaluation list -->
@if (selectedMember().evaluations?.length > 0) {
<div class="eval-list">
@for (ev of selectedMember().evaluations; track ev._id) {
<div class="eval-card">
<div class="eval-header">
<div class="eval-evaluator">
<strong>{{ ev.evaluatorId?.name }}</strong>
<span class="eval-role badge badge-muted">{{ ev.evaluatorRole | uppercase }}</span>
</div>
<span class="badge" [ngClass]="getDecisionClass(ev.recommendation)">{{ formatDecision(ev.recommendation) }}</span>
</div>
@if (ev.comments) {
<p class="eval-comments">"{{ ev.comments }}"</p>
}
<span class="eval-date">{{ ev.date | date:'medium' }}</span>
</div>
}
</div>
} @else {
<p class="text-muted">No evaluations yet</p>
}
<!-- Add evaluation form (HR / PM / Interviewer, not if already submitted or completed) -->
@if (!hasMemberEvaluated() && selectedMember().status !== 'completed') {
<div class="eval-form">
<h4>Add Your Evaluation</h4>
<div class="form-group">
<label class="form-label">Comments</label>
<textarea class="form-input form-textarea" [(ngModel)]="memberEvalComment"
placeholder="Enter your comments..." rows="3"></textarea>
</div>
<div class="form-group">
<label class="form-label">Recommendation *</label>
<select class="form-input" [(ngModel)]="memberEvalRecommendation">
<option value="">Select recommendation</option>
<option value="offer">Offer / Hire as Intern</option>
<option value="on_hold">On Hold</option>
<option value="rejected">Rejected</option>
<option value="2nd_round">2nd Round</option>
</select>
</div>
<button class="btn btn-primary" (click)="submitMemberEvaluation()"
[disabled]="!memberEvalRecommendation || isMemberSubmitting()">
@if (isMemberSubmitting()) { <span class="spinner spinner-sm"></span> Submitting... }
@else { Submit Evaluation }
</button>
</div>
}
</div>
<!-- Final Decision — Admin and HR, only when all evaluations done -->
@if ((authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') && allMemberEvaluationsDone() && selectedMember().status !== 'completed') {
<div class="detail-section decision-section">
<h3 class="detail-section-title">Final Decision</h3>
<div class="decision-buttons">
<button class="btn btn-success" (click)="setMemberDecision('accepted')">
<span class="material-symbols-rounded">check_circle</span> Accept
</button>
<button class="btn btn-warning" (click)="setMemberDecision('on_hold')">
<span class="material-symbols-rounded">pause_circle</span> On Hold
</button>
<button class="btn btn-danger" (click)="setMemberDecision('rejected')">
<span class="material-symbols-rounded">cancel</span> Reject
</button>
<button class="btn btn-outline" (click)="setMemberDecision('2nd_round')">
<span class="material-symbols-rounded">replay</span> 2nd Round
</button>
</div>
</div>
}
</div>
<div class="modal-footer">
<button class="btn btn-outline" (click)="closeMemberDetail()">Close</button>
</div>
</div>
</div>
}
<!-- Print Template for Evaluation PDF -->
@if (selectedMember()) {
<div class="print-container">
<div class="print-header">
<div class="print-header-left">
<h2 style="margin: 0; font-size: 20px;">Intern Interview Evaluation Form</h2>
</div>
<div class="print-header-right" style="text-align: right;">
<span style="color: #0078d4; font-weight: bold; font-size: 24px;">IDEAL</span><br>
<span style="font-size: 10px; color: #555;">TECH LABS</span>
</div>
</div>
<table class="print-table" style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold; width: 25%;">Candidate Name:</td>
<td style="border: 1px solid #000; padding: 8px; width: 25%;">{{ selectedMember().candidateId?.name }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold; width: 25%;">Date of Interview:</td>
<td style="border: 1px solid #000; padding: 8px; width: 25%;">{{ selectedMember().dateOfInterview | date:'mediumDate' }}</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Position:</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().position }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Source</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().source || '—' }}</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Tech Stack:</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().techStack || '—' }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Interviewer(s):</td>
<td style="border: 1px solid #000; padding: 8px;">
@if (selectedMember().assignedInterviewers?.length) {
@for (i of selectedMember().assignedInterviewers; track i._id; let last = $last) {
{{ i.name }}{{ !last ? ', ' : '' }}
}
} @else {
{{ selectedMember().interviewerId?.name || '—' }}
}
</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">General Aptitude Test QP Set</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().quizzes?.[0]?.title || '—' }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">General Aptitude Test Score</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().quizzes?.[0]?.completed ? selectedMember().quizzes[0].score + '/' + selectedMember().quizzes[0].totalMarks : 'Not Taken' }}</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Technical MCQ Test QP Set</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().quizzes?.[1]?.title || '—' }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Technical MCQ Test Score</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedMember().quizzes?.[1]?.completed ? selectedMember().quizzes[1].score + '/' + selectedMember().quizzes[1].totalMarks : 'Not Taken' }}</td>
</tr>
</table>
<!-- Render evaluations -->
@for (ev of selectedMember().evaluations; track ev._id) {
<div class="print-eval-block" style="margin-bottom: 30px; page-break-inside: avoid;">
<div class="print-eval-title" style="font-weight: bold; margin-bottom: 10px;">
{{ ev.evaluatorRole === 'hr' ? 'HR' : ev.evaluatorRole === 'pm' ? 'Project Manager' : 'Interviewer' }}'s Comments ({{ ev.evaluatorId?.name }}):
</div>
<div class="print-comments" style="min-height: 80px; margin-bottom: 15px;">
{{ ev.comments || 'No comments provided.' }}
</div>
<div class="print-recommendation" style="margin-bottom: 20px;">
<strong style="margin-right: 15px;">Recommendation:</strong>
<span style="margin-right: 15px;"><span style="font-size: 16px;">{{ ev.recommendation === 'offer' ? '☑' : '☐' }}</span> Offer/Hire as Intern</span>
<span style="margin-right: 15px;"><span style="font-size: 16px;">{{ ev.recommendation === 'on_hold' ? '☑' : '☐' }}</span> On Hold</span>
<span style="margin-right: 15px;"><span style="font-size: 16px;">{{ ev.recommendation === 'rejected' ? '☑' : '☐' }}</span> Rejected</span>
<span><span style="font-size: 16px;">{{ ev.recommendation === '2nd_round' ? '☑' : '☐' }}</span> 2nd Round</span>
</div>
<div class="print-signature-row" style="display: flex; justify-content: space-between; align-items: flex-end;">
<div class="print-signature" style="display: flex; align-items: flex-end;">
<strong>Evaluator's Signature:</strong>
@if (ev.evaluatorId?.signature) {
<img [src]="'http://localhost:5000' + ev.evaluatorId.signature" style="max-height: 40px; margin-left: 10px;">
} @else {
<span style="border-bottom: 1px solid #000; display: inline-block; width: 150px; margin-left: 10px;"></span>
}
</div>
<div class="print-date" style="display: flex; align-items: flex-end;">
<strong>Date:</strong>
<span style="border-bottom: 1px solid #000; display: inline-block; width: 120px; margin-left: 10px; text-align: center;">{{ ev.date | date:'shortDate' }}</span>
</div>
</div>
</div>
}
</div>
}
import { Component } from '@angular/core'; import { Component, OnInit, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { QuizService } from '../../../services/quiz.service';
import { AuthService } from '../../../services/auth.service';
export interface QuizEntry {
quizId: string;
}
export interface QuizSet {
quizEntries: QuizEntry[];
mode: 'random' | 'direct';
directAssignments: { [candidateId: string]: string };
}
@Component({ @Component({
selector: 'app-group-interview', selector: 'app-group-interview',
imports: [], standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './group-interview.html', templateUrl: './group-interview.html',
styleUrl: './group-interview.css', styleUrl: './group-interview.css'
}) })
export class GroupInterviewComponent {} export class GroupInterviewComponent implements OnInit {
// ── List state ────────────────────────────────────────────
interviews = signal<any[]>([]);
loading = signal(true);
filterStatus = '';
// ── Grouped view (one card per groupId) ──────────────────
groupedList = computed<any[]>(() => {
const map = new Map<string, any>();
for (const iv of this.interviews()) {
const key = iv.groupId || 'ungrouped';
if (!map.has(key)) {
map.set(key, {
groupId: key,
position: iv.position,
techStack: iv.techStack,
source: iv.source,
dateOfInterview: iv.dateOfInterview,
assignedInterviewers: iv.assignedInterviewers,
assignedHRs: iv.assignedHRs,
assignedPMs: iv.assignedPMs,
members: []
});
}
map.get(key)!.members.push(iv);
}
return Array.from(map.values());
});
stats = signal<any>({ total: 0, pending: 0, completed: 0, accepted: 0, rejected: 0 });
// ── Create modal state ────────────────────────────────────
showCreateModal = signal(false);
isSubmitting = signal(false);
successMsg = signal('');
// dropdown data
groups = signal<any[]>([]);
interviewers = signal<any[]>([]);
hrs = signal<any[]>([]);
pms = signal<any[]>([]);
quizzes = signal<any[]>([]);
groupMembers = signal<any[]>([]);
// form
newGroupInterview = this.blankForm();
// quiz-set builder
showQuizSetup = false;
pendingSetsCount = 2;
quizSets: QuizSet[] = [];
// ── Group detail modal state ─────────────────────────────
showDetailModal = signal(false);
selectedGroup = signal<any>(null);
// ── Per-candidate (member) detail modal ──────────────────
showMemberDetailModal = signal(false);
selectedMember = signal<any>(null); // full interview doc
memberEvalComment = '';
memberEvalRecommendation = '';
isMemberSubmitting = signal(false);
constructor(private quizService: QuizService, public authService: AuthService) {}
ngOnInit(): void {
this.loadInterviews();
this.loadStats();
}
// ── Data loaders ──────────────────────────────────────────
loadInterviews(): void {
this.loading.set(true);
const params: any = { type: 'group' };
if (this.filterStatus) params.status = this.filterStatus;
this.quizService.getInterviews(params).subscribe({
next: res => { this.interviews.set(res.interviews || []); this.loading.set(false); },
error: () => this.loading.set(false)
});
}
loadStats(): void {
this.quizService.getInterviewStats().subscribe({ next: res => this.stats.set(res) });
}
onFilterChange(): void { this.loadInterviews(); }
// ── Create modal ──────────────────────────────────────────
openCreateModal(): void {
this.newGroupInterview = this.blankForm();
this.quizSets = [];
this.showQuizSetup = false;
this.pendingSetsCount = 2;
this.groupMembers.set([]);
this.successMsg.set('');
this.quizService.getGroups().subscribe({ next: r => this.groups.set(r.groups || []) });
this.quizService.getInterviewers().subscribe({
next: r => {
const staff = r.interviewers || [];
this.interviewers.set(staff.filter((s: any) => s.role === 'interviewer'));
this.pms.set(staff.filter((s: any) => s.role === 'pm'));
this.hrs.set(staff.filter((s: any) => s.role === 'hr'));
}
});
this.quizService.getAdminQuizzes().subscribe({ next: r => this.quizzes.set(r.quizzes || []) });
this.showCreateModal.set(true);
}
closeCreateModal(): void {
this.showCreateModal.set(false);
this.successMsg.set('');
}
blankForm() {
return {
groupName: '',
assignedInterviewers: [] as string[],
assignedHRs: [] as string[],
assignedPMs: [] as string[],
position: '',
techStack: '',
source: '',
dateOfInterview: new Date().toISOString().split('T')[0]
};
}
toggleSelection(event: any, array: string[]): void {
const val = event.target.value;
if (event.target.checked) { array.push(val); }
else { const i = array.indexOf(val); if (i >= 0) array.splice(i, 1); }
}
onGroupChange(): void {
if (!this.newGroupInterview.groupName) { this.groupMembers.set([]); return; }
this.quizService.getGroupMembers(this.newGroupInterview.groupName).subscribe({
next: r => {
this.groupMembers.set(r.candidates || []);
// reset direct assignments when group changes
for (const set of this.quizSets) set.directAssignments = {};
}
});
}
// ── Quiz Sets builder ─────────────────────────────────────
confirmSetsCount(): void {
const n = Math.max(1, Math.min(10, this.pendingSetsCount || 1));
this.quizSets = Array.from({ length: n }, (_, i) => ({
quizEntries: [],
mode: 'random',
directAssignments: {}
}));
this.showQuizSetup = false;
}
addQuizToSet(setIdx: number): void {
this.quizSets[setIdx].quizEntries.push({ quizId: '' });
}
removeQuizEntry(setIdx: number, entryIdx: number): void {
this.quizSets[setIdx].quizEntries.splice(entryIdx, 1);
}
removeQuizSet(setIdx: number): void {
this.quizSets.splice(setIdx, 1);
}
addMoreSet(): void {
this.quizSets.push({ quizEntries: [], mode: 'random', directAssignments: {} });
}
/** Quizzes available for a specific dropdown (setIdx, entryIdx).
* Excludes any quizId already chosen in ANY other (set, entry) pair. */
getAvailableQuizzes(setIdx: number, entryIdx: number): any[] {
const usedIds = new Set<string>();
this.quizSets.forEach((set, si) => {
set.quizEntries.forEach((entry, ei) => {
if (entry.quizId && !(si === setIdx && ei === entryIdx)) usedIds.add(entry.quizId);
});
});
return this.quizzes().filter(q => !usedIds.has(q._id));
}
getQuizTitle(quizId: string): string {
return this.quizzes().find(q => q._id === quizId)?.title || quizId;
}
validEntries(set: QuizSet): QuizEntry[] {
return set.quizEntries.filter(e => e.quizId);
}
isSingleQuizSet(set: QuizSet): boolean {
return this.validEntries(set).length === 1;
}
isMultiQuizSet(set: QuizSet): boolean {
return this.validEntries(set).length > 1;
}
// ── Create interview ──────────────────────────────────────
createGroupInterview(): void {
if (!this.newGroupInterview.groupName || !this.newGroupInterview.position) return;
this.isSubmitting.set(true);
const payload = {
...this.newGroupInterview,
quizSets: this.quizSets.map(set => ({
quizEntries: set.quizEntries.filter(e => e.quizId),
mode: set.mode,
directAssignments: set.directAssignments
}))
};
this.quizService.createGroupInterview(payload).subscribe({
next: res => {
this.isSubmitting.set(false);
this.successMsg.set(res.message || `Interview created for ${res.count} candidates!`);
this.loadInterviews();
this.loadStats();
},
error: err => {
this.isSubmitting.set(false);
alert(err.error?.message || 'Failed to create group interview');
}
});
}
// ── Group detail modal ────────────────────────────────────
openDetail(group: any): void {
this.selectedGroup.set(group);
this.showDetailModal.set(true);
}
closeDetail(): void {
this.showDetailModal.set(false);
this.selectedGroup.set(null);
}
// ── Member (candidate) detail modal ──────────────────────
openMemberDetail(interviewId: string, event: Event): void {
event.stopPropagation();
this.quizService.getInterviewById(interviewId).subscribe({
next: res => {
this.selectedMember.set(res.interview);
this.memberEvalComment = '';
this.memberEvalRecommendation = '';
this.showMemberDetailModal.set(true);
}
});
}
closeMemberDetail(): void {
this.showMemberDetailModal.set(false);
this.selectedMember.set(null);
}
submitMemberEvaluation(): void {
const m = this.selectedMember();
if (!m || !this.memberEvalRecommendation) return;
this.isMemberSubmitting.set(true);
this.quizService.submitEvaluation(m._id, {
comments: this.memberEvalComment,
recommendation: this.memberEvalRecommendation
}).subscribe({
next: res => {
this.selectedMember.set(res.interview);
this.memberEvalComment = '';
this.memberEvalRecommendation = '';
this.isMemberSubmitting.set(false);
// refresh the group list so chips update
this.loadInterviews();
},
error: () => this.isMemberSubmitting.set(false)
});
}
setMemberDecision(decision: string): void {
const m = this.selectedMember();
if (!m) return;
this.quizService.setInterviewDecision(m._id, decision).subscribe({
next: res => {
this.selectedMember.set(res.interview);
this.loadInterviews();
this.loadStats();
}
});
}
hasMemberEvaluated(): boolean {
const m = this.selectedMember();
if (!m) return false;
const userId = this.authService.currentUser()?.id;
return m.evaluations?.some((e: any) => e.evaluatorId?._id === userId || e.evaluatorId === userId);
}
allMemberEvaluationsDone(): boolean {
const m = this.selectedMember();
if (!m) return false;
const total = (m.assignedInterviewers?.length || 0) +
(m.assignedHRs?.length || 0) +
(m.assignedPMs?.length || 0);
if (total === 0) return false;
return (m.evaluations?.length || 0) >= total;
}
/** True when THIS user still needs to evaluate this member */
needsEvaluation(m: any): boolean {
if (m.status === 'completed') return false;
const userId = this.authService.currentUser()?.id;
const role = this.authService.getUserRole();
if (role === 'admin') return false; // admin gives final decision, not evaluation
const evaluated = m.evaluations?.some(
(e: any) => e.evaluatorId?._id === userId || e.evaluatorId === userId
);
return !evaluated;
}
/** Whether ANY assigned staff member hasn't evaluated a given candidate interview yet */
hasPendingEvaluations(m: any): boolean {
if (m.status === 'completed') return false;
const total = (m.assignedInterviewers?.length || 0) +
(m.assignedHRs?.length || 0) +
(m.assignedPMs?.length || 0);
return (m.evaluations?.length || 0) < total;
}
isPdfGenerating = signal(false);
downloadEvaluationPdf(): void {
const m = this.selectedMember();
if (!m) return;
this.isPdfGenerating.set(true);
setTimeout(() => {
window.print();
this.isPdfGenerating.set(false);
}, 500);
}
// ── Helpers ───────────────────────────────────────────────
getStatusClass(status: string): string {
const map: any = {
pending: 'badge-warning', quiz_phase: 'badge-info',
coding_phase: 'badge-info', evaluation: 'badge-purple', completed: 'badge-success'
};
return map[status] || '';
}
getDecisionClass(decision: string): string {
const map: any = {
accepted: 'badge-success', rejected: 'badge-danger',
on_hold: 'badge-warning', '2nd_round': 'badge-info'
};
return map[decision] || 'badge-muted';
}
formatStatus(status: string): string {
return (status || '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
formatDecision(d: string): string {
const m: any = { pending: 'Pending', accepted: 'Accepted', rejected: 'Rejected', on_hold: 'On Hold', '2nd_round': '2nd Round' };
return m[d] || d;
}
groupStatusSummary(members: any[]): string {
const counts: any = {};
for (const m of members) counts[m.status] = (counts[m.status] || 0) + 1;
return Object.entries(counts).map(([s, c]) => `${this.formatStatus(s)}: ${c}`).join(' · ');
}
pendingCount(members: any[]): number { return members.filter(m => m.status !== 'completed').length; }
completedCount(members: any[]): number { return members.filter(m => m.status === 'completed').length; }
deleteGroupInterview(groupId: string): void {
if (!confirm(`Delete all interviews in group "${groupId}"?`)) return;
const ids = (this.groupedList().find(g => g.groupId === groupId)?.members || []).map((m: any) => m._id);
let done = 0;
for (const id of ids) {
this.quizService.deleteInterview(id).subscribe({ next: () => { done++; if (done === ids.length) { this.loadInterviews(); this.closeDetail(); } } });
}
}
setDecision(interviewId: string, decision: string): void {
this.quizService.setInterviewDecision(interviewId, decision).subscribe({
next: () => this.loadInterviews()
});
}
}
.page-container { padding: 32px 40px; }
.content-wrapper { max-width: 1200px; margin: 0; }
.page-header {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px;
}
.page-header h1 { font-size: 24px; font-weight: 700; color: var(--text-primary); margin: 0 0 4px; }
.page-subtitle { font-size: 14px; color: var(--text-muted); margin: 0; }
/* Stats Row */
.stats-row {
display: flex; gap: 16px; margin-bottom: 24px;
}
.mini-stat {
flex: 1; background: var(--bg-card); border: 1px solid var(--border-color);
border-radius: 12px; padding: 16px 20px; text-align: center;
}
.mini-stat-value { display: block; font-size: 28px; font-weight: 700; color: var(--text-primary); }
.mini-stat-value.orange { color: #f59e0b; }
.mini-stat-value.blue { color: #3b82f6; }
.mini-stat-value.green { color: #22c55e; }
.mini-stat-value.red { color: #ef4444; }
.mini-stat-label { font-size: 12px; color: var(--text-muted); font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; }
/* Filter */
.filter-bar { margin-bottom: 20px; }
.filter-select {
padding: 8px 14px; border: 1px solid var(--border-color); border-radius: 8px;
background: var(--bg-input); color: var(--text-primary); font-size: 14px; font-family: inherit;
}
/* Interview Cards */
.interview-list { display: flex; flex-direction: column; gap: 16px; }
.interview-card {
cursor: pointer; transition: all 0.2s; border: 1px solid var(--border-color);
border-radius: 16px; background: var(--bg-card);
}
.interview-card:hover { border-color: var(--accent-primary); box-shadow: 0 4px 16px rgba(102,126,234,0.1); transform: translateY(-1px); }
.iv-card-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
.iv-candidate { display: flex; align-items: center; gap: 14px; }
.iv-avatar {
width: 44px; height: 44px; border-radius: 12px;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex; align-items: center; justify-content: center;
color: #fff; font-weight: 700; font-size: 18px; flex-shrink: 0;
}
.iv-name { font-size: 16px; font-weight: 600; color: var(--text-primary); margin: 0; }
.iv-email { font-size: 13px; color: var(--text-muted); margin: 0; }
.iv-badges { display: flex; gap: 8px; }
.iv-card-meta {
display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 14px; padding-bottom: 14px;
border-bottom: 1px solid var(--border-color);
}
.meta-item {
display: inline-flex; align-items: center; gap: 6px;
font-size: 13px; color: var(--text-secondary);
}
.meta-item .material-symbols-rounded { font-size: 16px; color: var(--text-muted); }
/* Progress Steps */
.progress-steps { display: flex; align-items: center; }
.step { display: flex; align-items: center; gap: 6px; }
.step-dot {
width: 10px; height: 10px; border-radius: 50%; background: var(--border-color); transition: all 0.3s;
}
.step.done .step-dot { background: #22c55e; }
.step.active .step-dot { background: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,0.2); }
.step-label { font-size: 11px; color: var(--text-muted); font-weight: 500; }
.step.done .step-label { color: #22c55e; }
.step.active .step-label { color: #667eea; font-weight: 600; }
.step-line {
flex: 1; height: 2px; background: var(--border-color); margin: 0 8px; transition: background 0.3s;
}
.step-line.done { background: #22c55e; }
/* Badges */
.badge {
padding: 4px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
}
.badge-warning { background: rgba(245,158,11,0.1); color: #f59e0b; }
.badge-info { background: rgba(59,130,246,0.1); color: #3b82f6; }
.badge-purple { background: rgba(168,85,247,0.1); color: #a855f7; }
.badge-success { background: rgba(34,197,94,0.1); color: #22c55e; }
.badge-danger { background: rgba(239,68,68,0.1); color: #ef4444; }
.badge-muted { background: var(--bg-hover); color: var(--text-muted); }
/* Empty / Loading */
.loading-center { display: flex; flex-direction: column; align-items: center; padding: 80px; gap: 16px; color: var(--text-muted); }
.empty-state { text-align: center; padding: 80px; color: var(--text-muted); }
.empty-state .material-symbols-rounded { font-size: 56px; display: block; margin-bottom: 16px; opacity: 0.4; }
.empty-state h3 { color: var(--text-primary); font-size: 18px; margin: 0 0 8px; }
.empty-state p { font-size: 14px; margin: 0; }
/* Modal */
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.45); backdrop-filter: blur(4px);
z-index: 1000; display: flex; align-items: center; justify-content: center;
animation: fadeIn 0.2s ease-out;
}
.modal-container {
background: var(--bg-card); border-radius: 16px;
box-shadow: 0 10px 50px rgba(0,0,0,0.25); border: 1px solid var(--border-color);
width: 90%; max-height: 85vh; overflow-y: auto;
animation: slideUp 0.3s cubic-bezier(0.16,1,0.3,1);
}
.modal-lg { max-width: 640px; }
.modal-xl { max-width: 800px; }
.modal-header {
padding: 20px 24px; border-bottom: 1px solid var(--border-color);
display: flex; justify-content: space-between; align-items: center;
position: sticky; top: 0; background: var(--bg-card); z-index: 2; border-radius: 16px 16px 0 0;
}
.modal-header h2 { font-size: 18px; font-weight: 600; margin: 0 12px 0 0; display: inline; }
.modal-body { padding: 24px; }
.modal-footer {
padding: 16px 24px; border-top: 1px solid var(--border-color);
display: flex; justify-content: flex-end; gap: 12px;
background: var(--bg-hover); position: sticky; bottom: 0; border-radius: 0 0 16px 16px;
}
/* Form */
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }
.form-group { display: flex; flex-direction: column; gap: 6px; }
.form-label { font-size: 13px; font-weight: 600; color: var(--text-primary); }
.form-input {
padding: 10px 14px; border: 1px solid var(--border-color); border-radius: 8px;
background: var(--bg-input); color: var(--text-primary); font-size: 14px; font-family: inherit;
transition: all 0.2s; width: 100%; box-sizing: border-box;
}
.form-input:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px rgba(102,126,234,0.1); }
.form-textarea { resize: vertical; min-height: 80px; }
/* Quiz Selector */
.quiz-select-grid { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.quiz-select-item {
display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px;
border: 1px solid var(--border-color); border-radius: 8px;
cursor: pointer; font-size: 13px; color: var(--text-secondary); transition: all 0.2s;
}
.quiz-select-item .material-symbols-rounded { font-size: 18px; }
.quiz-select-item.selected { border-color: #667eea; background: rgba(102,126,234,0.08); color: #667eea; }
.quiz-select-item:hover { border-color: var(--accent-primary); }
/* Detail */
.detail-body { display: flex; flex-direction: column; gap: 24px; }
.detail-section { padding-bottom: 16px; border-bottom: 1px solid var(--border-color); }
.detail-section:last-child { border-bottom: none; }
.detail-section-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin: 0 0 16px; text-transform: uppercase; letter-spacing: 0.5px; }
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.detail-item { display: flex; flex-direction: column; gap: 2px; }
.detail-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
.detail-value { font-size: 14px; color: var(--text-primary); font-weight: 500; }
/* Quiz Results */
.quiz-results-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.quiz-result-card {
padding: 14px; border: 1px solid var(--border-color); border-radius: 10px;
background: var(--bg-hover);
}
.qr-title { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; }
.qr-score { font-size: 14px; color: #22c55e; font-weight: 700; }
.qr-pending { font-size: 13px; color: var(--text-muted); font-style: italic; }
/* Evaluations */
.eval-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 20px; }
.eval-card {
padding: 16px; border: 1px solid var(--border-color); border-radius: 10px;
background: var(--bg-hover);
}
.eval-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.eval-evaluator { display: flex; align-items: center; gap: 8px; }
.eval-evaluator strong { font-size: 14px; color: var(--text-primary); }
.eval-role { font-size: 10px; }
.eval-comments { font-size: 14px; color: var(--text-secondary); margin: 8px 0; font-style: italic; line-height: 1.5; }
.eval-date { font-size: 11px; color: var(--text-muted); }
.eval-form {
margin-top: 16px; padding: 20px; border: 1px dashed rgba(102,126,234,0.3);
border-radius: 12px; background: rgba(102,126,234,0.03);
}
.eval-form h4 { font-size: 14px; font-weight: 600; color: var(--text-primary); margin: 0 0 16px; }
/* Decision */
.decision-section { padding: 16px; background: rgba(102,126,234,0.03); border-radius: 12px; border: 1px dashed rgba(102,126,234,0.2); }
.decision-buttons { display: flex; gap: 12px; flex-wrap: wrap; }
.btn-success { background: #22c55e; color: #fff; }
.btn-success:hover { background: #16a34a; }
.btn-warning { background: #f59e0b; color: #fff; }
.btn-warning:hover { background: #d97706; }
.btn-danger { background: #ef4444; color: #fff; }
.btn-danger:hover { background: #dc2626; }
.text-muted { color: var(--text-muted); font-size: 13px; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@media (max-width: 768px) {
.page-container { padding: 20px; }
.stats-row { flex-wrap: wrap; }
.mini-stat { min-width: 120px; }
.form-row { grid-template-columns: 1fr; }
.detail-grid { grid-template-columns: 1fr; }
}
/* Print Styles */
.print-container {
display: none;
}
@media print {
body * {
visibility: hidden;
}
.print-container, .print-container * {
visibility: visible;
}
.print-container {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
padding: 20px;
background: white;
color: black;
font-family: Arial, sans-serif;
}
.print-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #0078d4;
padding-bottom: 10px;
margin-bottom: 20px;
}
}
<p>individual-interview works!</p> <div class="page-container animate-fade-in">
<div class="content-wrapper">
<div class="page-header">
<div>
<h1>Individual Interviews</h1>
<p class="page-subtitle">Manage one-on-one candidate interview evaluations</p>
</div>
<button class="btn btn-primary" (click)="openCreateModal()">
<span class="material-symbols-rounded">add</span>
New Interview
</button>
</div>
<!-- Stats -->
<div class="stats-row">
<div class="mini-stat">
<span class="mini-stat-value">{{ stats().total }}</span>
<span class="mini-stat-label">Total</span>
</div>
<div class="mini-stat">
<span class="mini-stat-value orange">{{ stats().pending }}</span>
<span class="mini-stat-label">In Progress</span>
</div>
<div class="mini-stat">
<span class="mini-stat-value blue">{{ stats().completed }}</span>
<span class="mini-stat-label">Completed</span>
</div>
<div class="mini-stat">
<span class="mini-stat-value green">{{ stats().accepted }}</span>
<span class="mini-stat-label">Accepted</span>
</div>
<div class="mini-stat">
<span class="mini-stat-value red">{{ stats().rejected }}</span>
<span class="mini-stat-label">Rejected</span>
</div>
</div>
<!-- Filter -->
<div class="filter-bar">
<select class="filter-select" [(ngModel)]="filterStatus" (ngModelChange)="onFilterChange()">
<option value="">All Statuses</option>
<option value="pending">Pending</option>
<option value="quiz_phase">Quiz Phase</option>
<option value="coding_phase">Coding Phase</option>
<option value="evaluation">Evaluation</option>
<option value="completed">Completed</option>
</select>
</div>
<!-- Interview List -->
@if (loading()) {
<div class="loading-center"><div class="spinner spinner-lg"></div><p>Loading interviews...</p></div>
} @else if (interviews().length === 0) {
<div class="empty-state">
<span class="material-symbols-rounded">work_off</span>
<h3>No interviews found</h3>
<p>Create your first interview to get started</p>
</div>
} @else {
<div class="interview-list">
@for (iv of interviews(); track iv._id) {
<div class="interview-card card card-padding" (click)="openDetail(iv)">
<div class="iv-card-top">
<div class="iv-candidate">
<div class="iv-avatar">{{ iv.candidateId?.name?.charAt(0)?.toUpperCase() }}</div>
<div>
<h3 class="iv-name">{{ iv.candidateId?.name }}</h3>
<p class="iv-email">{{ iv.candidateId?.email }}</p>
</div>
</div>
<div class="iv-badges">
<span class="badge" [ngClass]="getStatusClass(iv.status)">{{ formatStatus(iv.status) }}</span>
@if (iv.finalDecision !== 'pending') {
<span class="badge" [ngClass]="getDecisionClass(iv.finalDecision)">{{ formatDecision(iv.finalDecision) }}</span>
}
</div>
</div>
<div class="iv-card-meta">
<span class="meta-item"><span class="material-symbols-rounded">work</span> {{ iv.position }}</span>
@if (iv.techStack) {
<span class="meta-item"><span class="material-symbols-rounded">code</span> {{ iv.techStack }}</span>
}
<span class="meta-item"><span class="material-symbols-rounded">person</span> {{ iv.interviewerId?.name }}</span>
<span class="meta-item"><span class="material-symbols-rounded">calendar_today</span> {{ iv.dateOfInterview | date:'mediumDate' }}</span>
</div>
<div class="iv-card-progress">
<div class="progress-steps">
<div class="step" [class.active]="iv.status === 'pending'" [class.done]="['quiz_phase','coding_phase','evaluation','completed'].includes(iv.status)">
<span class="step-dot"></span><span class="step-label">Created</span>
</div>
<div class="step-line" [class.done]="['quiz_phase','coding_phase','evaluation','completed'].includes(iv.status)"></div>
<div class="step" [class.active]="iv.status === 'quiz_phase'" [class.done]="['coding_phase','evaluation','completed'].includes(iv.status)">
<span class="step-dot"></span><span class="step-label">Quiz</span>
</div>
<div class="step-line" [class.done]="['coding_phase','evaluation','completed'].includes(iv.status)"></div>
<div class="step" [class.active]="iv.status === 'coding_phase'" [class.done]="['evaluation','completed'].includes(iv.status)">
<span class="step-dot"></span><span class="step-label">Coding</span>
</div>
<div class="step-line" [class.done]="['evaluation','completed'].includes(iv.status)"></div>
<div class="step" [class.active]="iv.status === 'evaluation'" [class.done]="iv.status === 'completed'">
<span class="step-dot"></span><span class="step-label">Evaluate</span>
</div>
<div class="step-line" [class.done]="iv.status === 'completed'"></div>
<div class="step" [class.active]="iv.status === 'completed'" [class.done]="iv.status === 'completed'">
<span class="step-dot"></span><span class="step-label">Done</span>
</div>
</div>
</div>
</div>
}
</div>
}
</div>
</div>
<!-- Create Interview Modal -->
@if (showCreateModal()) {
<div class="modal-overlay" (click)="closeCreateModal()">
<div class="modal-container modal-lg" (click)="$event.stopPropagation()">
<div class="modal-header">
<h2>Create Individual Interview</h2>
<button class="icon-btn" (click)="closeCreateModal()"><span class="material-symbols-rounded">close</span></button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group">
<label class="form-label">Candidate *</label>
<select class="form-input" [(ngModel)]="newInterview.candidateId">
<option value="">Select candidate</option>
@for (c of candidates(); track c._id) {
<option [value]="c._id">{{ c.name }} ({{ c.email }})</option>
}
</select>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1;">
<label class="form-label">Interviewers</label>
<div class="quiz-select-grid" style="max-height: 120px; overflow-y: auto;">
@for (i of interviewers(); track i._id) {
<label class="quiz-select-item" style="cursor: pointer;">
<input type="checkbox" [value]="i._id" (change)="toggleSelection($event, newInterview.assignedInterviewers)" style="margin-right: 8px;"> {{ i.name }}
</label>
}
@if (interviewers().length === 0) { <p class="text-muted" style="font-size: 12px;">No interviewers found</p> }
</div>
</div>
<div class="form-group" style="flex: 1;">
<label class="form-label">Project Managers</label>
<div class="quiz-select-grid" style="max-height: 120px; overflow-y: auto;">
@for (p of pms(); track p._id) {
<label class="quiz-select-item" style="cursor: pointer;">
<input type="checkbox" [value]="p._id" (change)="toggleSelection($event, newInterview.assignedPMs)" style="margin-right: 8px;"> {{ p.name }}
</label>
}
@if (pms().length === 0) { <p class="text-muted" style="font-size: 12px;">No PMs found</p> }
</div>
</div>
<div class="form-group" style="flex: 1;">
<label class="form-label">HRs</label>
<div class="quiz-select-grid" style="max-height: 120px; overflow-y: auto;">
@for (h of hrs(); track h._id) {
<label class="quiz-select-item" style="cursor: pointer;">
<input type="checkbox" [value]="h._id" (change)="toggleSelection($event, newInterview.assignedHRs)" style="margin-right: 8px;"> {{ h.name }}
</label>
}
@if (hrs().length === 0) { <p class="text-muted" style="font-size: 12px;">No HRs found</p> }
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Position *</label>
<input class="form-input" [(ngModel)]="newInterview.position" placeholder="e.g., Software Intern">
</div>
<div class="form-group">
<label class="form-label">Tech Stack</label>
<input class="form-input" [(ngModel)]="newInterview.techStack" placeholder="e.g., React, Node.js">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Source</label>
<input class="form-input" [(ngModel)]="newInterview.source" placeholder="e.g., LinkedIn, Campus">
</div>
<div class="form-group">
<label class="form-label">Date of Interview</label>
<input class="form-input" type="date" [(ngModel)]="newInterview.dateOfInterview">
</div>
</div>
<!-- Quiz Selection -->
<div class="form-group">
<label class="form-label">Assign Quizzes (optional)</label>
<div class="quiz-select-grid">
@for (q of quizzes(); track q._id) {
<div class="quiz-select-item" [class.selected]="isQuizSelected(q._id)" (click)="toggleQuizSelection(q._id)">
<span class="material-symbols-rounded">{{ isQuizSelected(q._id) ? 'check_box' : 'check_box_outline_blank' }}</span>
<span>{{ q.title }}</span>
</div>
}
@if (quizzes().length === 0) {
<p class="text-muted">No quizzes available</p>
}
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-outline" (click)="closeCreateModal()">Cancel</button>
<button class="btn btn-primary" (click)="createInterview()" [disabled]="isSubmitting() || !newInterview.candidateId || !newInterview.position">
@if (isSubmitting()) { <span class="spinner spinner-sm"></span> Creating... } @else { Create Interview }
</button>
</div>
</div>
</div>
}
<!-- Interview Detail Modal -->
@if (showDetailModal() && selectedInterview()) {
<div class="modal-overlay" (click)="closeDetail()">
<div class="modal-container modal-xl" (click)="$event.stopPropagation()">
<div class="modal-header">
<div>
<h2>Interview Detail</h2>
<span class="badge" [ngClass]="getStatusClass(selectedInterview().status)">{{ formatStatus(selectedInterview().status) }}</span>
@if (selectedInterview().finalDecision !== 'pending') {
<span class="badge" [ngClass]="getDecisionClass(selectedInterview().finalDecision)" style="margin-left:8px">{{ formatDecision(selectedInterview().finalDecision) }}</span>
}
</div>
<button class="icon-btn" (click)="closeDetail()"><span class="material-symbols-rounded">close</span></button>
</div>
<div class="modal-body detail-body">
<!-- Candidate Info -->
<div class="detail-section">
<h3 class="detail-section-title">Candidate Information</h3>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Name</span>
<span class="detail-value">{{ selectedInterview().candidateId?.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Email</span>
<span class="detail-value">{{ selectedInterview().candidateId?.email }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Position</span>
<span class="detail-value">{{ selectedInterview().position }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Tech Stack</span>
<span class="detail-value">{{ selectedInterview().techStack || '—' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Source</span>
<span class="detail-value">{{ selectedInterview().source || '—' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Interviewer</span>
<span class="detail-value">{{ selectedInterview().interviewerId?.name }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Date of Interview</span>
<span class="detail-value">{{ selectedInterview().dateOfInterview | date:'mediumDate' }}</span>
</div>
</div>
</div>
<!-- Quiz Results -->
@if (selectedInterview().quizzes?.length > 0) {
<div class="detail-section">
<h3 class="detail-section-title">Quiz Results</h3>
<div class="quiz-results-grid">
@for (q of selectedInterview().quizzes; track q.quizId) {
<div class="quiz-result-card">
<div class="qr-title">{{ q.title }}</div>
@if (q.completed) {
<div class="qr-score">{{ q.score }}/{{ q.totalMarks }} ({{ q.percentage }}%)</div>
} @else {
<div class="qr-pending">Not Taken</div>
}
</div>
}
</div>
</div>
}
<!-- Coding Round Submission -->
<div class="detail-section">
<h3 class="detail-section-title">Coding Challenge Submission</h3>
@if (selectedInterview().codingRound?.zipFile) {
<div class="eval-card" style="display: flex; justify-content: space-between; align-items: center;">
<div>
<span class="material-symbols-rounded" style="color: var(--accent-primary); vertical-align: middle; margin-right: 8px; font-size: 24px;">folder_zip</span>
<strong>Submitted on:</strong> {{ selectedInterview().codingRound.submittedAt | date:'medium' }}
</div>
<div style="display: flex; gap: 12px; align-items: center;">
@if (selectedInterview().codingRound.validated) {
<span class="badge badge-success">Validated by {{ selectedInterview().codingRound.validatedBy?.name || 'Reviewer' }}</span>
}
<a [href]="'http://localhost:5000' + selectedInterview().codingRound.zipFile" target="_blank" class="btn btn-primary" style="padding: 6px 12px; font-size: 14px; border-radius: 6px;">
<span class="material-symbols-rounded" style="font-size: 18px; margin-right: 4px;">download</span> Download
</a>
</div>
</div>
} @else {
<div class="quiz-results-grid">
<div class="quiz-result-card" style="border: 1px dashed var(--border-color); background: var(--bg-body);">
<div class="qr-title">Coding Challenge</div>
<div class="qr-pending">Not Submitted</div>
</div>
</div>
}
</div>
<!-- Evaluations -->
<div class="detail-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3 class="detail-section-title" style="margin: 0;">Evaluations</h3>
@if (allEvaluationsDone()) {
<button class="btn btn-primary" (click)="downloadEvaluationPdf()">
@if (isPdfGenerating()) { <span class="spinner spinner-sm"></span> Generating... } @else { <span class="material-symbols-rounded">download</span> Download PDF }
</button>
}
</div>
@if (selectedInterview().evaluations?.length > 0) {
<div class="eval-list">
@for (ev of selectedInterview().evaluations; track ev._id) {
<div class="eval-card">
<div class="eval-header">
<div class="eval-evaluator">
<strong>{{ ev.evaluatorId?.name }}</strong>
<span class="eval-role badge badge-muted">{{ ev.evaluatorRole | uppercase }}</span>
</div>
<span class="badge" [ngClass]="getDecisionClass(ev.recommendation)">{{ formatDecision(ev.recommendation) }}</span>
</div>
@if (ev.comments) {
<p class="eval-comments">"{{ ev.comments }}"</p>
}
<span class="eval-date">{{ ev.date | date:'medium' }}</span>
</div>
}
</div>
} @else {
<p class="text-muted">No evaluations yet</p>
}
<!-- Add evaluation form -->
@if (!hasUserEvaluated() && selectedInterview().status !== 'completed') {
<div class="eval-form">
<h4>Add Your Evaluation</h4>
<div class="form-group">
<label class="form-label">Comments</label>
<textarea class="form-input form-textarea" [(ngModel)]="evalComment" placeholder="Enter your comments..." rows="3"></textarea>
</div>
<div class="form-group">
<label class="form-label">Recommendation *</label>
<select class="form-input" [(ngModel)]="evalRecommendation">
<option value="">Select recommendation</option>
<option value="offer">Offer / Hire as Intern</option>
<option value="on_hold">On Hold</option>
<option value="rejected">Rejected</option>
<option value="2nd_round">2nd Round</option>
</select>
</div>
<button class="btn btn-primary" (click)="submitEvaluation()" [disabled]="!evalRecommendation() || isSubmitting()">
Submit Evaluation
</button>
</div>
}
</div>
<!-- Final Decision (admin and hr only) -->
@if ((authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') && selectedInterview().status !== 'completed') {
<div class="detail-section decision-section">
<h3 class="detail-section-title">Final Decision</h3>
<div class="decision-buttons">
<button class="btn btn-success" (click)="setDecision('accepted')">
<span class="material-symbols-rounded">check_circle</span> Accept
</button>
<button class="btn btn-warning" (click)="setDecision('on_hold')">
<span class="material-symbols-rounded">pause_circle</span> On Hold
</button>
<button class="btn btn-danger" (click)="setDecision('rejected')">
<span class="material-symbols-rounded">cancel</span> Reject
</button>
<button class="btn btn-outline" (click)="setDecision('2nd_round')">
<span class="material-symbols-rounded">replay</span> 2nd Round
</button>
</div>
</div>
}
</div>
<div class="modal-footer">
@if (authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') {
<button class="btn btn-danger btn-sm" (click)="deleteInterview(selectedInterview()._id)">Delete Interview</button>
}
<button class="btn btn-outline" (click)="closeDetail()">Close</button>
</div>
</div>
</div>
}
<!-- Print Template for Evaluation PDF -->
@if (selectedInterview()) {
<div class="print-container">
<div class="print-header">
<div class="print-header-left">
<h2 style="margin: 0; font-size: 20px;">Intern Interview Evaluation Form</h2>
</div>
<div class="print-header-right" style="text-align: right;">
<span style="color: #0078d4; font-weight: bold; font-size: 24px;">IDEAL</span><br>
<span style="font-size: 10px; color: #555;">TECH LABS</span>
</div>
</div>
<table class="print-table" style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold; width: 25%;">Candidate Name:</td>
<td style="border: 1px solid #000; padding: 8px; width: 25%;">{{ selectedInterview().candidateId?.name }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold; width: 25%;">Date of Interview:</td>
<td style="border: 1px solid #000; padding: 8px; width: 25%;">{{ selectedInterview().dateOfInterview | date:'mediumDate' }}</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Position:</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedInterview().position }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Source</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedInterview().source || '—' }}</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Tech Stack:</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedInterview().techStack || '—' }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Interviewer(s):</td>
<td style="border: 1px solid #000; padding: 8px;">
@if (selectedInterview().assignedInterviewers?.length) {
@for (i of selectedInterview().assignedInterviewers; track i._id; let last = $last) {
{{ i.name }}{{ !last ? ', ' : '' }}
}
} @else {
{{ selectedInterview().interviewerId?.name || '—' }}
}
</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">General Aptitude Test QP Set</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedInterview().quizzes?.[0]?.title || '—' }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">General Aptitude Test Score</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedInterview().quizzes?.[0]?.completed ? selectedInterview().quizzes[0].score + '/' + selectedInterview().quizzes[0].totalMarks : 'Not Taken' }}</td>
</tr>
<tr>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Technical MCQ Test QP Set</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedInterview().quizzes?.[1]?.title || '—' }}</td>
<td style="border: 1px solid #000; padding: 8px; font-weight: bold;">Technical MCQ Test Score</td>
<td style="border: 1px solid #000; padding: 8px;">{{ selectedInterview().quizzes?.[1]?.completed ? selectedInterview().quizzes[1].score + '/' + selectedInterview().quizzes[1].totalMarks : 'Not Taken' }}</td>
</tr>
</table>
<!-- Render evaluations -->
@for (ev of selectedInterview().evaluations; track ev._id) {
<div class="print-eval-block" style="margin-bottom: 30px; page-break-inside: avoid;">
<div class="print-eval-title" style="font-weight: bold; margin-bottom: 10px;">
{{ ev.evaluatorRole === 'hr' ? 'HR' : ev.evaluatorRole === 'pm' ? 'Project Manager' : 'Interviewer' }}'s Comments ({{ ev.evaluatorId?.name }}):
</div>
<div class="print-comments" style="min-height: 80px; margin-bottom: 15px;">
{{ ev.comments || 'No comments provided.' }}
</div>
<div class="print-recommendation" style="margin-bottom: 20px;">
<strong style="margin-right: 15px;">Recommendation:</strong>
<span style="margin-right: 15px;"><span style="font-size: 16px;">{{ ev.recommendation === 'offer' ? '☑' : '☐' }}</span> Offer/Hire as Intern</span>
<span style="margin-right: 15px;"><span style="font-size: 16px;">{{ ev.recommendation === 'on_hold' ? '☑' : '☐' }}</span> On Hold</span>
<span style="margin-right: 15px;"><span style="font-size: 16px;">{{ ev.recommendation === 'rejected' ? '☑' : '☐' }}</span> Rejected</span>
<span><span style="font-size: 16px;">{{ ev.recommendation === '2nd_round' ? '☑' : '☐' }}</span> 2nd Round</span>
</div>
<div class="print-signature-row" style="display: flex; justify-content: space-between; align-items: flex-end;">
<div class="print-signature" style="display: flex; align-items: flex-end;">
<strong>Evaluator's Signature:</strong>
@if (ev.evaluatorId?.signature) {
<img [src]="'http://localhost:5000' + ev.evaluatorId.signature" style="max-height: 40px; margin-left: 10px;">
} @else {
<span style="border-bottom: 1px solid #000; display: inline-block; width: 150px; margin-left: 10px;"></span>
}
</div>
<div class="print-date" style="display: flex; align-items: flex-end;">
<strong>Date:</strong>
<span style="border-bottom: 1px solid #000; display: inline-block; width: 120px; margin-left: 10px; text-align: center;">{{ ev.date | date:'shortDate' }}</span>
</div>
</div>
</div>
}
</div>
}
import { Component } from '@angular/core'; import { Component, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { QuizService } from '../../../services/quiz.service';
import { AuthService } from '../../../services/auth.service';
@Component({ @Component({
selector: 'app-individual-interview', selector: 'app-individual-interview',
imports: [], standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './individual-interview.html', templateUrl: './individual-interview.html',
styleUrl: './individual-interview.css', styleUrl: './individual-interview.css'
}) })
export class IndividualInterview {} export class IndividualInterviewComponent implements OnInit {
interviews = signal<any[]>([]);
loading = signal(true);
showCreateModal = signal(false);
showDetailModal = signal(false);
selectedInterview = signal<any>(null);
// Create form data
candidates = signal<any[]>([]);
interviewers = signal<any[]>([]);
hrs = signal<any[]>([]);
pms = signal<any[]>([]);
quizzes = signal<any[]>([]);
newInterview = {
candidateId: '',
assignedInterviewers: [] as string[],
assignedHRs: [] as string[],
assignedPMs: [] as string[],
position: '',
techStack: '',
source: '',
dateOfInterview: new Date().toISOString().split('T')[0],
quizIds: [] as string[]
};
// Evaluation form
evalComment = signal('');
evalRecommendation = signal('');
// Stats
stats = signal<any>({ total: 0, pending: 0, completed: 0, accepted: 0, rejected: 0 });
// Filter
filterStatus = signal('');
isSubmitting = signal(false);
constructor(private quizService: QuizService, public authService: AuthService) {}
ngOnInit(): void {
this.loadInterviews();
this.loadStats();
}
loadInterviews(): void {
this.loading.set(true);
const params: any = {};
if (this.filterStatus()) params.status = this.filterStatus();
params.type = 'individual';
this.quizService.getInterviews(params).subscribe({
next: (res) => {
this.interviews.set(res.interviews || []);
this.loading.set(false);
},
error: () => this.loading.set(false)
});
}
loadStats(): void {
this.quizService.getInterviewStats().subscribe({
next: (res) => this.stats.set(res)
});
}
openCreateModal(): void {
// Load dropdown data
this.quizService.getInterviewCandidates().subscribe({
next: (res) => this.candidates.set(res.candidates || [])
});
this.quizService.getInterviewers().subscribe({
next: (res) => {
const staff = res.interviewers || [];
this.interviewers.set(staff.filter((s: any) => s.role === 'interviewer'));
this.pms.set(staff.filter((s: any) => s.role === 'pm'));
this.hrs.set(staff.filter((s: any) => s.role === 'hr'));
}
});
this.quizService.getAdminQuizzes().subscribe({
next: (res) => this.quizzes.set(res.quizzes || [])
});
this.newInterview = {
candidateId: '', assignedInterviewers: [], assignedHRs: [], assignedPMs: [], position: '', techStack: '',
source: '', dateOfInterview: new Date().toISOString().split('T')[0], quizIds: []
};
this.showCreateModal.set(true);
}
closeCreateModal(): void {
this.showCreateModal.set(false);
}
toggleQuizSelection(quizId: string): void {
const idx = this.newInterview.quizIds.indexOf(quizId);
if (idx >= 0) {
this.newInterview.quizIds.splice(idx, 1);
} else {
this.newInterview.quizIds.push(quizId);
}
}
toggleSelection(event: any, array: string[]): void {
const val = event.target.value;
if (event.target.checked) {
array.push(val);
} else {
const idx = array.indexOf(val);
if (idx >= 0) array.splice(idx, 1);
}
}
isQuizSelected(quizId: string): boolean {
return this.newInterview.quizIds.includes(quizId);
}
createInterview(): void {
if (!this.newInterview.candidateId || !this.newInterview.position) return;
this.isSubmitting.set(true);
this.quizService.createInterview(this.newInterview).subscribe({
next: () => {
this.isSubmitting.set(false);
this.closeCreateModal();
this.loadInterviews();
this.loadStats();
},
error: (err) => {
this.isSubmitting.set(false);
alert(err.error?.message || 'Failed to create interview');
}
});
}
openDetail(interview: any): void {
this.quizService.getInterviewById(interview._id).subscribe({
next: (res) => {
this.selectedInterview.set(res.interview);
this.evalComment.set('');
this.evalRecommendation.set('');
this.showDetailModal.set(true);
}
});
}
closeDetail(): void {
this.showDetailModal.set(false);
this.selectedInterview.set(null);
}
submitEvaluation(): void {
const interview = this.selectedInterview();
if (!interview || !this.evalRecommendation()) return;
this.isSubmitting.set(true);
this.quizService.submitEvaluation(interview._id, {
comments: this.evalComment(),
recommendation: this.evalRecommendation()
}).subscribe({
next: (res) => {
this.selectedInterview.set(res.interview);
this.evalComment.set('');
this.evalRecommendation.set('');
this.isSubmitting.set(false);
},
error: () => this.isSubmitting.set(false)
});
}
setDecision(decision: string): void {
const interview = this.selectedInterview();
if (!interview) return;
this.quizService.setInterviewDecision(interview._id, decision).subscribe({
next: (res) => {
this.selectedInterview.set(res.interview);
this.loadInterviews();
this.loadStats();
}
});
}
deleteInterview(id: string): void {
if (confirm('Are you sure you want to delete this interview?')) {
this.quizService.deleteInterview(id).subscribe({
next: () => {
this.loadInterviews();
this.loadStats();
this.closeDetail();
}
});
}
}
onFilterChange(): void {
this.loadInterviews();
}
getStatusClass(status: string): string {
switch (status) {
case 'pending': return 'badge-warning';
case 'quiz_phase': return 'badge-info';
case 'coding_phase': return 'badge-info';
case 'evaluation': return 'badge-purple';
case 'completed': return 'badge-success';
default: return '';
}
}
getDecisionClass(decision: string): string {
switch (decision) {
case 'accepted': return 'badge-success';
case 'rejected': return 'badge-danger';
case 'on_hold': return 'badge-warning';
case '2nd_round': return 'badge-info';
default: return 'badge-muted';
}
}
formatStatus(status: string): string {
return status.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
formatDecision(decision: string): string {
const map: any = {
pending: 'Pending', accepted: 'Accepted', rejected: 'Rejected',
on_hold: 'On Hold', '2nd_round': '2nd Round'
};
return map[decision] || decision;
}
hasUserEvaluated(): boolean {
const interview = this.selectedInterview();
if (!interview) return false;
const userId = this.authService.currentUser()?.id;
return interview.evaluations?.some((e: any) => e.evaluatorId?._id === userId);
}
isPdfGenerating = signal(false);
allEvaluationsDone(): boolean {
const iv = this.selectedInterview();
if (!iv) return false;
const numInterviewers = iv.assignedInterviewers?.length || 0;
const numHrs = iv.assignedHRs?.length || 0;
const numPms = iv.assignedPMs?.length || 0;
const totalExpected = numInterviewers + numHrs + numPms;
if (totalExpected === 0) return false;
const numEvaluations = iv.evaluations?.length || 0;
return numEvaluations >= totalExpected;
}
downloadEvaluationPdf(): void {
const iv = this.selectedInterview();
if (!iv) return;
this.isPdfGenerating.set(true);
setTimeout(() => {
window.print();
this.isPdfGenerating.set(false);
}, 500);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment