diff --git a/README.md b/README.md
new file mode 100644
index 00000000..7ad97e53
--- /dev/null
+++ b/README.md
@@ -0,0 +1,84 @@
+# Facultyware Survey System
+
+## Deskripsi Aplikasi
+
+Facultyware Survey System merupakan aplikasi berbasis web yang digunakan untuk mengelola survey secara digital. Sistem menyediakan fitur manajemen survey, pertanyaan, opsi jawaban, assignment pertanyaan ke survey, REST API, export PDF, serta autentikasi pengguna menggunakan session.
+
+Fitur utama yang tersedia:
+
+- Login dan Logout
+- CRUD Survey
+- Publish Survey
+- Search dan Pagination Survey
+- Export PDF Survey
+- REST API Survey
+- CRUD Pertanyaan Survey
+- REST API Pertanyaan
+- CRUD Opsi Jawaban
+- CRUD Assignment Pertanyaan ke Survey
+- Validasi Form
+- Session Based Access Control
+
+---
+
+## Cara Instalasi dan Menjalankan Aplikasi
+
+### Clone Repository
+
+```bash
+git clone https://github.com/hanifalhaj05-a11y/facultyware.git
+cd facultyware
+```
+
+### Install Dependency
+
+```bash
+npm install
+```
+
+### Buat Database MySQL
+
+```sql
+CREATE DATABASE facultyware;
+```
+
+Import file database ke database `facultyware`.
+
+### Konfigurasi Environment
+
+Buat file `.env`
+
+```env
+DB_HOST=localhost
+DB_USER=root
+DB_PASSWORD=
+DB_NAME=facultyware
+PORT=3000
+SESSION_SECRET=facultyware
+```
+
+### Menjalankan Aplikasi
+
+```bash
+npm start
+```
+
+atau
+
+```bash
+node app.js
+```
+
+Aplikasi dapat diakses melalui:
+
+```text
+http://localhost:3000
+```
+
+---
+
+## Pembagian Tugas Anggota
+
+| NIM | Nama | Tugas |
+|------|------|--------|
+| 2411523023 | Hanif Al Haj | Implementasi Authentication, Session Based Access Control, CRUD Survey, CRUD Pertanyaan, CRUD Opsi Jawaban, CRUD Assignment, REST API, Export PDF, Validasi Form, Testing Playwright, Deployment |
diff --git a/app.js b/app.js
index f91917a2..46a774ca 100644
--- a/app.js
+++ b/app.js
@@ -8,6 +8,11 @@ var MySQLStore = require('express-mysql-session')(session);
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
+var surveyRouter = require('./routes/survey');
+var questionRouter = require('./routes/question');
+var optionRouter = require("./routes/option");
+var assignmentRouter = require("./routes/assignment");
+var apiRouter = require("./routes/api");
const { notFoundHandler, errorHandler } = require('./middlewares/error');
var app = express();
@@ -43,7 +48,11 @@ app.use(session({
app.use('/', indexRouter);
app.use('/users', usersRouter);
-
+app.use('/survey', surveyRouter);
+app.use('/question', questionRouter);
+app.use('/option', optionRouter);
+app.use('/assignment', assignmentRouter);
+app.use('/api', apiRouter);
// catch 404 and forward to error handler
app.use(notFoundHandler);
diff --git a/controllers/api/questionApiController.js b/controllers/api/questionApiController.js
new file mode 100644
index 00000000..b68f1dd3
--- /dev/null
+++ b/controllers/api/questionApiController.js
@@ -0,0 +1,29 @@
+const db = require("../../lib/db");
+
+const index = async (req, res, next) => {
+
+ try {
+
+ const [questions] = await db.query(`
+ SELECT
+ id,
+ question_text,
+ type,
+ is_active,
+ created_at,
+ updated_at
+ FROM survey_questions
+ ORDER BY id DESC
+ `);
+
+ res.status(200).json(questions);
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+module.exports = {
+ index
+};
\ No newline at end of file
diff --git a/controllers/api/surveyApiController.js b/controllers/api/surveyApiController.js
new file mode 100644
index 00000000..4c06bdaf
--- /dev/null
+++ b/controllers/api/surveyApiController.js
@@ -0,0 +1,21 @@
+const db = require("../../lib/db");
+
+const index = async (req, res, next) => {
+
+ try {
+
+ const [rows] = await db.query(
+ "SELECT * FROM surveys ORDER BY id DESC"
+ );
+
+ res.json(rows);
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+module.exports = {
+ index
+};
\ No newline at end of file
diff --git a/controllers/assignmentController.js b/controllers/assignmentController.js
new file mode 100644
index 00000000..3652b4a3
--- /dev/null
+++ b/controllers/assignmentController.js
@@ -0,0 +1,322 @@
+const db = require("../lib/db");
+
+// =========================================
+// LIST ASSIGNMENT
+// =========================================
+const index = async (req, res, next) => {
+
+ try {
+
+ const search = req.query.search || "";
+ const page = parseInt(req.query.page) || 1;
+
+ const limit = 5;
+ const offset = (page - 1) * limit;
+
+ const [countRows] = await db.query(
+ `
+ SELECT COUNT(*) AS total
+ FROM survey_question_assignments
+ JOIN surveys
+ ON survey_question_assignments.survey_id = surveys.id
+ JOIN survey_questions
+ ON survey_question_assignments.survey_question_id = survey_questions.id
+ WHERE surveys.title LIKE ?
+ OR survey_questions.question_text LIKE ?
+ `,
+ [
+ `%${search}%`,
+ `%${search}%`
+ ]
+ );
+
+ const totalData = countRows[0].total;
+ const totalPage = Math.ceil(totalData / limit);
+
+ const [assignments] = await db.query(
+ `
+ SELECT
+ survey_question_assignments.*,
+ surveys.title AS survey_title,
+ survey_questions.question_text
+ FROM survey_question_assignments
+ JOIN surveys
+ ON survey_question_assignments.survey_id = surveys.id
+ JOIN survey_questions
+ ON survey_question_assignments.survey_question_id = survey_questions.id
+ WHERE surveys.title LIKE ?
+ OR survey_questions.question_text LIKE ?
+ ORDER BY survey_question_assignments.id DESC
+ LIMIT ?
+ OFFSET ?
+ `,
+ [
+ `%${search}%`,
+ `%${search}%`,
+ limit,
+ offset
+ ]
+ );
+
+ res.render("assignment/index", {
+ title: "Assignment",
+ user: req.session.name,
+ assignments,
+ search,
+ page,
+ totalPage
+ });
+
+ } catch (err) {
+
+ next(err);
+
+ }
+
+};
+
+// =========================================
+// FORM CREATE
+// =========================================
+
+const createForm = async (req, res, next) => {
+
+ try {
+
+ const [surveys] = await db.query(
+ "SELECT * FROM surveys ORDER BY title ASC"
+ );
+
+ const [questions] = await db.query(
+ "SELECT * FROM survey_questions ORDER BY question_text ASC"
+ );
+
+ res.render("assignment/create", {
+ title: "Create Assignment",
+ user: req.session.name,
+ surveys,
+ questions,
+ error: null
+ });
+
+ } catch (err) {
+
+ next(err);
+
+ }
+
+};
+
+// =========================================
+// STORE
+// =========================================
+
+const store = async (req, res, next) => {
+
+ const {
+ survey_id,
+ survey_question_id,
+ order
+ } = req.body;
+
+ try {
+
+ if (!survey_id || !survey_question_id || !order) {
+
+ const [surveys] = await db.query(
+ "SELECT * FROM surveys ORDER BY title ASC"
+ );
+
+ const [questions] = await db.query(
+ "SELECT * FROM survey_questions ORDER BY question_text ASC"
+ );
+
+ return res.render("assignment/create", {
+ title: "Create Assignment",
+ user: req.session.name,
+ surveys,
+ questions,
+ error: "Semua field wajib diisi."
+ });
+
+ }
+
+ await db.query(
+ `
+ INSERT INTO survey_question_assignments
+ (
+ survey_id,
+ survey_question_id,
+ \`order\`,
+ created_at,
+ updated_at
+ )
+ VALUES
+ (?, ?, ?, NOW(), NOW())
+ `,
+ [
+ survey_id,
+ survey_question_id,
+ order
+ ]
+ );
+
+ res.redirect("/assignment");
+
+ } catch (err) {
+
+ next(err);
+
+ }
+
+};
+
+// =========================================
+// FORM EDIT
+// =========================================
+
+const editForm = async (req, res, next) => {
+
+ try {
+
+ const [rows] = await db.query(
+ "SELECT * FROM survey_question_assignments WHERE id=?",
+ [req.params.id]
+ );
+
+ if (rows.length === 0) {
+
+ return res.redirect("/assignment");
+
+ }
+
+ const [surveys] = await db.query(
+ "SELECT * FROM surveys ORDER BY title ASC"
+ );
+
+ const [questions] = await db.query(
+ "SELECT * FROM survey_questions ORDER BY question_text ASC"
+ );
+
+ res.render("assignment/edit", {
+ title: "Edit Assignment",
+ user: req.session.name,
+ assignment: rows[0],
+ surveys,
+ questions,
+ error: null
+ });
+
+ } catch (err) {
+
+ next(err);
+
+ }
+
+};
+
+// =========================================
+// UPDATE
+// =========================================
+
+const update = async (req, res, next) => {
+
+ const {
+ survey_id,
+ survey_question_id,
+ order
+ } = req.body;
+
+ try {
+
+ const [rows] = await db.query(
+ "SELECT * FROM survey_question_assignments WHERE id=?",
+ [req.params.id]
+ );
+
+ if (rows.length === 0) {
+
+ return res.redirect("/assignment");
+
+ }
+
+ if (!survey_id || !survey_question_id || !order) {
+
+ const [surveys] = await db.query(
+ "SELECT * FROM surveys ORDER BY title ASC"
+ );
+
+ const [questions] = await db.query(
+ "SELECT * FROM survey_questions ORDER BY question_text ASC"
+ );
+
+ return res.render("assignment/edit", {
+ title: "Edit Assignment",
+ user: req.session.name,
+ assignment: rows[0],
+ surveys,
+ questions,
+ error: "Semua field wajib diisi."
+ });
+
+ }
+
+ await db.query(
+ `
+ UPDATE survey_question_assignments
+ SET
+ survey_id=?,
+ survey_question_id=?,
+ \`order\`=?,
+ updated_at=NOW()
+ WHERE id=?
+ `,
+ [
+ survey_id,
+ survey_question_id,
+ order,
+ req.params.id
+ ]
+ );
+
+ res.redirect("/assignment");
+
+ } catch (err) {
+
+ next(err);
+
+ }
+
+};
+
+// =========================================
+// DELETE
+// =========================================
+
+const destroy = async (req, res, next) => {
+
+ try {
+
+ await db.query(
+ "DELETE FROM survey_question_assignments WHERE id=?",
+ [req.params.id]
+ );
+
+ res.redirect("/assignment");
+
+ } catch (err) {
+
+ next(err);
+
+ }
+
+};
+
+module.exports = {
+ index,
+ createForm,
+ store,
+ editForm,
+ update,
+ destroy
+};
\ No newline at end of file
diff --git a/controllers/indexController.js b/controllers/indexController.js
index 5ea918c1..6598a714 100644
--- a/controllers/indexController.js
+++ b/controllers/indexController.js
@@ -1,69 +1,161 @@
const bcrypt = require("bcryptjs");
const db = require("../lib/db");
+/**
+ * Halaman awal
+ */
const index = (req, res) => {
- res.render("index", { title: "Express" });
-};
+ if (req.session.userId) {
+ return res.redirect("/home");
+ }
-const home = (req, res) => {
- res.render("home", { title: "Home", user: req.session.username });
+ res.redirect("/login");
};
+/**
+ * Halaman Home
+ */
+const home = async (req, res, next) => {
+
+ try {
+
+ const [[survey]] = await db.query(
+ "SELECT COUNT(*) total FROM surveys"
+ );
+
+ const [[question]] = await db.query(
+ "SELECT COUNT(*) total FROM survey_questions"
+ );
+
+ const [[option]] = await db.query(
+ "SELECT COUNT(*) total FROM survey_question_options"
+);
+
+ const [[assignment]] = await db.query(
+ "SELECT COUNT(*) total FROM survey_question_assignments"
+ );
+
+ res.render("home", {
+ title: "Dashboard",
+ user: req.session.name,
+
+ totalSurvey: survey.total,
+ totalQuestion: question.total,
+ totalOption: option.total,
+ totalAssignment: assignment.total
+
+ });
+
+ } catch (err) {
+
+ next(err);
+
+ }
+
+};
+/**
+ * Halaman Login
+ */
const loginPage = (req, res) => {
- if (req.session.userId) {
- return res.redirect("/home");
- }
- res.render("login", { title: "Login", error: null });
+
+ if (req.session.userId) {
+ return res.redirect("/home");
+ }
+
+ res.render("login", {
+ title: "Login",
+ error: null,
+ });
+
};
+/**
+ * Proses Login
+ */
const login = async (req, res, next) => {
- const { username, password } = req.body;
- try {
- const [rows] = await db.query("SELECT * FROM users WHERE username = ?", [
- username,
- ]);
+ const {
+ email,
+ password
+ } = req.body;
- if (rows.length === 0) {
- return res.render("login", {
- title: "Login",
- error: "Invalid username or password",
- });
- }
+ try {
- const user = rows[0];
- const isMatch = await bcrypt.compare(password, user.password);
+ const [rows] = await db.query(
+ "SELECT * FROM users WHERE email=?",
+ [email]
+ );
+
+ if (rows.length === 0) {
+
+ return res.render("login", {
+ title: "Login",
+ error: "Email atau password salah"
+ });
+
+ }
+
+ const user = rows[0];
+
+ const isMatch = await bcrypt.compare(
+ password,
+ user.password
+ );
+
+ if (!isMatch) {
+
+ return res.render("login", {
+ title: "Login",
+ error: "Email atau password salah"
+ });
+
+ }
+
+ req.session.userId = user.id;
+ req.session.name = user.name;
+ req.session.email = user.email;
+
+ res.redirect("/home");
- if (!isMatch) {
- return res.render("login", {
- title: "Login",
- error: "Invalid username or password",
- });
}
- // Set session
- req.session.userId = user.id;
- req.session.username = user.username;
+ catch (err) {
+
+ next(err);
+
+ }
- res.redirect("/home");
- } catch (err) {
- next(err);
- }
};
+/**
+ * Logout
+ */
const logout = (req, res, next) => {
- req.session.destroy((err) => {
- if (err) {
- return next(err);
- }
- res.redirect("/login");
- });
+
+ req.session.destroy((err) => {
+
+ if (err) {
+
+ return next(err);
+
+ }
+
+ res.redirect("/login");
+
+ });
+
};
module.exports = {
- index,
- home,
- loginPage,
- login,
- logout
-};
+
+ index,
+
+ home,
+
+ loginPage,
+
+ login,
+
+ logout
+
+};
\ No newline at end of file
diff --git a/controllers/optionController.js b/controllers/optionController.js
new file mode 100644
index 00000000..a82fde82
--- /dev/null
+++ b/controllers/optionController.js
@@ -0,0 +1,252 @@
+const db = require("../lib/db");
+
+// =========================================
+// LIST SEMUA OPTION
+// =========================================
+const all = async (req, res, next) => {
+
+ try {
+
+ const [options] = await db.query(`
+ SELECT
+ survey_question_options.*,
+ survey_questions.question_text
+ FROM survey_question_options
+ JOIN survey_questions
+ ON survey_questions.id = survey_question_options.survey_question_id
+ ORDER BY survey_question_options.id DESC
+ `);
+
+ res.render("option/all", {
+ title: "Option",
+ user: req.session.name,
+ options
+ });
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+// =========================================
+// LIST OPTION BERDASARKAN QUESTION
+// =========================================
+const index = async (req, res, next) => {
+
+ try {
+
+ const questionId = req.params.id;
+
+ const [questionRows] = await db.query(
+ "SELECT * FROM survey_questions WHERE id=?",
+ [questionId]
+ );
+
+ if (questionRows.length === 0) {
+ return res.redirect("/question");
+ }
+
+ const [options] = await db.query(`
+ SELECT *
+ FROM survey_question_options
+ WHERE survey_question_id=?
+ ORDER BY weight ASC,id ASC
+ `,[questionId]);
+
+ res.render("option/index",{
+ title:"Option",
+ user:req.session.name,
+ question:questionRows[0],
+ options
+ });
+
+ } catch(err){
+ next(err);
+ }
+
+};
+
+// =========================================
+// CREATE
+// =========================================
+const createForm = async (req,res,next)=>{
+
+ try{
+
+ const questionId=req.params.questionId;
+
+ const [rows]=await db.query(
+ "SELECT * FROM survey_questions WHERE id=?",
+ [questionId]
+ );
+
+ if(rows.length===0){
+ return res.redirect("/question");
+ }
+
+ res.render("option/create",{
+ title:"Tambah Option",
+ user:req.session.name,
+ question:rows[0],
+ error:null
+ });
+
+ }catch(err){
+ next(err);
+ }
+
+};
+
+// =========================================
+// STORE
+// =========================================
+const store = async (req,res,next)=>{
+
+ try{
+
+ const questionId=req.params.questionId;
+
+ const{
+ option_text,
+ weight
+ }=req.body;
+
+ await db.query(`
+ INSERT INTO survey_question_options
+ (
+ survey_question_id,
+ option_text,
+ weight,
+ created_at,
+ updated_at
+ )
+ VALUES
+ (?,?,?,NOW(),NOW())
+ `,[
+ questionId,
+ option_text,
+ weight
+ ]);
+
+ res.redirect("/option/question/"+questionId);
+
+ }catch(err){
+ next(err);
+ }
+
+};
+
+// =========================================
+// EDIT
+// =========================================
+const editForm = async (req,res,next)=>{
+
+ try{
+
+ const [rows]=await db.query(
+ "SELECT * FROM survey_question_options WHERE id=?",
+ [req.params.id]
+ );
+
+ if(rows.length===0){
+ return res.redirect("/option");
+ }
+
+ res.render("option/edit",{
+ title:"Edit Option",
+ user:req.session.name,
+ option:rows[0],
+ error:null
+ });
+
+ }catch(err){
+ next(err);
+ }
+
+};
+
+// =========================================
+// UPDATE
+// =========================================
+const update = async (req,res,next)=>{
+
+ try{
+
+ const{
+ option_text,
+ weight
+ }=req.body;
+
+ const [rows]=await db.query(
+ "SELECT survey_question_id FROM survey_question_options WHERE id=?",
+ [req.params.id]
+ );
+
+ if(rows.length===0){
+ return res.redirect("/option");
+ }
+
+ const questionId=rows[0].survey_question_id;
+
+ await db.query(`
+ UPDATE survey_question_options
+ SET
+ option_text=?,
+ weight=?,
+ updated_at=NOW()
+ WHERE id=?
+ `,[
+ option_text,
+ weight,
+ req.params.id
+ ]);
+
+ res.redirect("/option/question/"+questionId);
+
+ }catch(err){
+ next(err);
+ }
+
+};
+
+// =========================================
+// DELETE
+// =========================================
+const destroy = async (req,res,next)=>{
+
+ try{
+
+ const [rows]=await db.query(
+ "SELECT survey_question_id FROM survey_question_options WHERE id=?",
+ [req.params.id]
+ );
+
+ if(rows.length===0){
+ return res.redirect("/option");
+ }
+
+ const questionId=rows[0].survey_question_id;
+
+ await db.query(
+ "DELETE FROM survey_question_options WHERE id=?",
+ [req.params.id]
+ );
+
+ res.redirect("/option/question/"+questionId);
+
+ }catch(err){
+ next(err);
+ }
+
+};
+
+module.exports={
+ all,
+ index,
+ createForm,
+ store,
+ editForm,
+ update,
+ destroy
+};
\ No newline at end of file
diff --git a/controllers/questionController.js b/controllers/questionController.js
new file mode 100644
index 00000000..3e416343
--- /dev/null
+++ b/controllers/questionController.js
@@ -0,0 +1,321 @@
+const db = require("../lib/db");
+
+const index = async (req, res, next) => {
+ try {
+ const search = req.query.search || "";
+ const page = parseInt(req.query.page) || 1;
+ const limit = 5;
+ const offset = (page - 1) * limit;
+
+ const [countResult] = await db.query(
+ `
+ SELECT COUNT(*) AS total
+ FROM survey_questions
+ WHERE question_text LIKE ?
+ `,
+ [`%${search}%`]
+ );
+
+ const totalData = countResult[0].total;
+ const totalPage = Math.ceil(totalData / limit);
+
+ const [questions] = await db.query(
+ `
+ SELECT *
+ FROM survey_questions
+ WHERE question_text LIKE ?
+ ORDER BY id DESC
+ LIMIT ?
+ OFFSET ?
+ `,
+ [
+ `%${search}%`,
+ limit,
+ offset
+ ]
+ );
+
+ res.render("question/index", {
+ title: "Question",
+ user: req.session.name,
+ questions,
+ search,
+ page,
+ totalPage
+ });
+
+ } catch (err) {
+ next(err);
+ }
+};
+
+// =====================================
+// LIST QUESTION BY SURVEY
+// =====================================
+
+const bySurvey = async (req, res, next) => {
+ try {
+
+ const surveyId = req.params.id;
+
+ const [surveyRows] = await db.query(
+ "SELECT * FROM surveys WHERE id=?",
+ [surveyId]
+ );
+
+ // ✅ FIX IMPORTANT (WAJIB)
+ if (!surveyRows || surveyRows.length === 0) {
+ return res.redirect("/survey");
+ }
+
+ const [questions] = await db.query(
+ `
+ SELECT
+ survey_questions.*,
+ survey_question_assignments.id AS assignment_id,
+ survey_question_assignments.\`order\`
+ FROM survey_question_assignments
+ JOIN survey_questions
+ ON survey_question_assignments.survey_question_id = survey_questions.id
+ WHERE survey_question_assignments.survey_id=?
+ ORDER BY survey_question_assignments.\`order\`
+ `,
+ [surveyId]
+ );
+
+ res.render("question/bySurvey", {
+ title: "Question List",
+ user: req.session.name,
+ survey: surveyRows[0],
+ questions: questions || []
+ });
+
+ } catch (err) {
+ next(err);
+ }
+};
+
+// =====================================
+// CREATE
+// =====================================
+
+const createForm = (req, res) => {
+
+ res.render("question/create", {
+ title: "Create Question",
+ user: req.session.name,
+ error: null,
+ surveyId: req.params.surveyId
+ });
+
+};
+
+const store = async (req, res, next) => {
+
+ const { question_text, type } = req.body;
+ const surveyId = req.params.surveyId;
+
+ if (!question_text || question_text.trim() === "") {
+
+ return res.render("question/create", {
+ title: "Create Question",
+ user: req.session.name,
+ error: "Question is required.",
+ surveyId
+ });
+
+ }
+
+ try {
+
+ const [result] = await db.query(
+ `
+ INSERT INTO survey_questions
+ (
+ question_text,
+ type,
+ is_active,
+ created_at,
+ updated_at
+ )
+ VALUES
+ (?, ?, 1, NOW(), NOW())
+ `,
+ [
+ question_text,
+ type
+ ]
+ );
+
+ const questionId = result.insertId;
+
+ const [rows] = await db.query(
+ `
+ SELECT COALESCE(MAX(\`order\`),0)+1 AS nextOrder
+ FROM survey_question_assignments
+ WHERE survey_id=?
+ `,
+ [surveyId]
+ );
+
+ await db.query(
+ `
+ INSERT INTO survey_question_assignments
+ (
+ survey_id,
+ survey_question_id,
+ \`order\`,
+ created_at,
+ updated_at
+ )
+ VALUES
+ (?, ?, ?, NOW(), NOW())
+ `,
+ [
+ surveyId,
+ questionId,
+ rows[0].nextOrder
+ ]
+ );
+
+ res.redirect("/question/survey/" + surveyId);
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+// =====================================
+// EDIT
+// =====================================
+
+const editForm = async (req, res, next) => {
+
+ try {
+
+ const [rows] = await db.query(
+ "SELECT * FROM survey_questions WHERE id=?",
+ [req.params.id]
+ );
+
+ res.render("question/edit", {
+ title: "Edit Question",
+ user: req.session.name,
+ question: rows[0],
+ error: null
+ });
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+const update = async (req, res, next) => {
+
+ const { question_text, type } = req.body;
+
+ try {
+
+ if (!question_text || question_text.trim() === "") {
+
+ const [rows] = await db.query(
+ "SELECT * FROM survey_questions WHERE id=?",
+ [req.params.id]
+ );
+
+ return res.render("question/edit", {
+ title: "Edit Question",
+ user: req.session.name,
+ question: rows[0],
+ error: "Question is required."
+ });
+
+ }
+
+ await db.query(
+ `
+ UPDATE survey_questions
+ SET
+ question_text=?,
+ type=?,
+ updated_at=NOW()
+ WHERE id=?
+ `,
+ [
+ question_text,
+ type,
+ req.params.id
+ ]
+ );
+
+ res.redirect("/question");
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+// =====================================
+// DELETE
+// =====================================
+
+const destroy = async (req, res, next) => {
+
+ try {
+
+ const id = req.params.id;
+
+ const [assignment] = await db.query(
+ `
+ SELECT survey_id
+ FROM survey_question_assignments
+ WHERE survey_question_id=?
+ LIMIT 1
+ `,
+ [id]
+ );
+
+ const surveyId =
+ assignment.length > 0
+ ? assignment[0].survey_id
+ : null;
+
+ await db.query(
+ "DELETE FROM survey_question_options WHERE survey_question_id=?",
+ [id]
+ );
+
+ await db.query(
+ "DELETE FROM survey_question_assignments WHERE survey_question_id=?",
+ [id]
+ );
+
+ await db.query(
+ "DELETE FROM survey_questions WHERE id=?",
+ [id]
+ );
+
+ if (surveyId) {
+ return res.redirect("/question/survey/" + surveyId);
+ }
+
+ res.redirect("/question");
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+module.exports = {
+ index,
+ bySurvey,
+ createForm,
+ store,
+ editForm,
+ update,
+ destroy
+};
\ No newline at end of file
diff --git a/controllers/surveyController.js b/controllers/surveyController.js
new file mode 100644
index 00000000..de8c7ecb
--- /dev/null
+++ b/controllers/surveyController.js
@@ -0,0 +1,315 @@
+const db = require("../lib/db");
+const PDFDocument = require("pdfkit");
+
+/* =========================
+ LIST SURVEY
+========================= */
+const index = async (req, res, next) => {
+ try {
+
+ const search = req.query.search || "";
+ const page = parseInt(req.query.page) || 1;
+
+ const limit = 5;
+ const offset = (page - 1) * limit;
+
+ const [countResult] = await db.query(
+ `
+ SELECT COUNT(*) total
+ FROM surveys
+ WHERE title LIKE ?
+ `,
+ [`%${search}%`]
+ );
+
+ const totalData = countResult[0].total;
+ const totalPage = Math.ceil(totalData / limit);
+
+ const [surveys] = await db.query(
+ `
+ SELECT
+ surveys.*,
+ COUNT(survey_question_assignments.id) AS total_question
+ FROM surveys
+ LEFT JOIN survey_question_assignments
+ ON surveys.id = survey_question_assignments.survey_id
+ WHERE surveys.title LIKE ?
+ GROUP BY surveys.id
+ ORDER BY surveys.created_at DESC
+ LIMIT ?
+ OFFSET ?
+ `,
+ [
+ `%${search}%`,
+ limit,
+ offset
+ ]
+ );
+
+ res.render("survey/index", {
+ title: "Survey",
+ user: req.session.name,
+ surveys,
+ search,
+ page,
+ totalPage
+ });
+
+ } catch (err) {
+ next(err);
+ }
+};
+
+/* =========================
+ CREATE
+========================= */
+
+const createForm = (req, res) => {
+
+ res.render("survey/create", {
+ title: "Tambah Survey",
+ user: req.session.name
+ });
+
+};
+
+/* =========================
+ STORE
+========================= */
+
+const store = async (req, res, next) => {
+
+ const {
+ title,
+ description,
+ start_date,
+ end_date
+ } = req.body;
+
+ try {
+
+ await db.query(
+ `
+ INSERT INTO surveys
+ (
+ title,
+ description,
+ start_date,
+ end_date,
+ created_by,
+ employee_id,
+ created_at,
+ updated_at
+ )
+ VALUES
+ (?, ?, ?, ?, ?, ?, NOW(), NOW())
+ `,
+ [
+ title,
+ description,
+ start_date,
+ end_date,
+ req.session.userId,
+ req.session.userId
+ ]
+ );
+
+ res.redirect("/survey");
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+/* =========================
+ EDIT
+========================= */
+
+const editForm = async (req, res, next) => {
+
+ try {
+
+ const [rows] = await db.query(
+ "SELECT * FROM surveys WHERE id=?",
+ [req.params.id]
+ );
+
+ res.render("survey/edit", {
+ title: "Edit Survey",
+ user: req.session.name,
+ survey: rows[0]
+ });
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+/* =========================
+ UPDATE
+========================= */
+
+const update = async (req, res, next) => {
+
+ const {
+ title,
+ description,
+ start_date,
+ end_date
+ } = req.body;
+
+ try {
+
+ await db.query(
+ `
+ UPDATE surveys
+ SET
+ title=?,
+ description=?,
+ start_date=?,
+ end_date=?,
+ updated_at=NOW()
+ WHERE id=?
+ `,
+ [
+ title,
+ description,
+ start_date,
+ end_date,
+ req.params.id
+ ]
+ );
+
+ res.redirect("/survey");
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+/* =========================
+ DELETE
+========================= */
+
+const destroy = async (req, res, next) => {
+
+ try {
+
+ await db.query(
+ "DELETE FROM survey_question_assignments WHERE survey_id=?",
+ [req.params.id]
+ );
+
+ await db.query(
+ "DELETE FROM surveys WHERE id=?",
+ [req.params.id]
+ );
+
+ res.redirect("/survey");
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+/* =========================
+ PUBLISH
+========================= */
+
+const publish = async (req, res, next) => {
+
+ try {
+
+ await db.query(
+ `
+ UPDATE surveys
+ SET
+ is_active = IF(is_active=1,0,1),
+ updated_at = NOW()
+ WHERE id=?
+ `,
+ [req.params.id]
+ );
+
+ res.redirect("/survey");
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+/* =========================
+ EXPORT PDF
+========================= */
+
+const exportPDF = async (req, res, next) => {
+
+ try {
+
+ const [surveys] = await db.query(
+ `
+ SELECT *
+ FROM surveys
+ ORDER BY created_at DESC
+ `
+ );
+
+ const doc = new PDFDocument({
+ margin: 40,
+ size: "A4"
+ });
+
+ res.setHeader("Content-Type", "application/pdf");
+
+ res.setHeader(
+ "Content-Disposition",
+ "attachment; filename=survey.pdf"
+ );
+
+ doc.pipe(res);
+
+ doc
+ .fontSize(18)
+ .text("LAPORAN DATA SURVEY", {
+ align: "center"
+ });
+
+ doc.moveDown();
+
+ surveys.forEach((survey, index) => {
+
+ doc.fontSize(12);
+
+ doc.text(`${index + 1}. ${survey.title}`);
+ doc.text(`Deskripsi : ${survey.description}`);
+ doc.text(`Tanggal Mulai : ${new Date(survey.start_date).toLocaleDateString("id-ID")}`);
+ doc.text(`Tanggal Selesai : ${new Date(survey.end_date).toLocaleDateString("id-ID")}`);
+ doc.text(`Status : ${survey.is_active ? "Aktif" : "Nonaktif"}`);
+
+ doc.moveDown();
+
+ });
+
+ doc.end();
+
+ } catch (err) {
+ next(err);
+ }
+
+};
+
+module.exports = {
+ index,
+ createForm,
+ store,
+ editForm,
+ update,
+ destroy,
+ publish,
+ exportPDF
+};
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 59b6794c..a907c312 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"name": "central-panel",
"version": "0.0.0",
"dependencies": {
+ "@fortawesome/fontawesome-free": "^7.2.0",
+ "@tailwindcss/cli": "^4.3.1",
"bcryptjs": "^3.0.3",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
@@ -18,7 +20,701 @@
"express-session": "^1.19.0",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
- "mysql2": "^3.22.3"
+ "mysql2": "^3.22.3",
+ "pdfkit": "^0.19.1",
+ "tailwindcss": "^4.3.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.61.0"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-free": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz",
+ "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==",
+ "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@noble/ciphers": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
+ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
+ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+ "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+ "cpu": [
+ "arm64"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+ "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+ "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+ "cpu": [
+ "x64"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+ "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+ "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+ "cpu": [
+ "arm"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+ "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+ "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+ "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+ "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+ "cpu": [
+ "x64"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+ "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+ "cpu": [
+ "x64"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+ "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+ "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.61.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz",
+ "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.61.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.23",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
+ "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@tailwindcss/cli": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.3.1.tgz",
+ "integrity": "sha512-ZWPy20rF+TBfTImxDMG3Wr75Y3RpaPlo9lc+oJbInlMyjT+XPkTVKVIL5RZ7JirXuIahcfHoLNFRmDorKi+JQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@parcel/watcher": "2.5.1",
+ "@tailwindcss/node": "4.3.1",
+ "@tailwindcss/oxide": "4.3.1",
+ "enhanced-resolve": "5.21.6",
+ "mri": "^1.2.0",
+ "picocolors": "^1.1.1",
+ "tailwindcss": "4.3.1"
+ },
+ "bin": {
+ "tailwindcss": "dist/index.mjs"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
+ "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
+ "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-x64": "4.3.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
+ "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
+ "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
+ "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
+ "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
+ "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
+ "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
+ "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
+ "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
+ "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
+ "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
+ "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
+ "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
}
},
"node_modules/@types/node": {
@@ -59,6 +755,26 @@
"node": ">= 6.0.0"
}
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
@@ -101,6 +817,36 @@
"node": ">= 0.8"
}
},
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brotli": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
+ "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.1.2"
+ }
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "~1.0.5"
+ }
+ },
"node_modules/bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
@@ -110,6 +856,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
@@ -180,13 +935,31 @@
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
"license": "MIT",
"engines": {
- "node": ">= 0.6"
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==",
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "license": "Apache-2.0",
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
}
},
- "node_modules/destroy": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
- "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==",
+ "node_modules/dfa": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
+ "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/dotenv": {
@@ -225,6 +998,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -421,6 +1207,24 @@
"node": ">= 0.6"
}
},
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/finalhandler": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
@@ -439,6 +1243,23 @@
"node": ">= 0.8"
}
},
+ "node_modules/fontkit": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
+ "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.5.12",
+ "brotli": "^1.3.2",
+ "clone": "^2.1.2",
+ "dfa": "^1.2.0",
+ "fast-deep-equal": "^3.1.3",
+ "restructure": "^3.0.0",
+ "tiny-inflate": "^1.0.3",
+ "unicode-properties": "^1.4.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -457,6 +1278,21 @@
"node": ">= 0.6"
}
},
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
@@ -466,6 +1302,12 @@
"is-property": "^1.0.2"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
"node_modules/http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
@@ -508,12 +1350,346 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-md5": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
+ "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==",
+ "license": "MIT"
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss/node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/linebreak": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
+ "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "0.0.8",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/linebreak/node_modules/base64-js": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
+ "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -544,6 +1720,15 @@
"url": "https://github.com/sponsors/wellwelwel"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -568,6 +1753,19 @@
"node": ">= 0.6"
}
},
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
"node_modules/mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
@@ -614,6 +1812,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -679,6 +1886,12 @@
"node": ">= 0.6"
}
},
+ "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==",
+ "license": "MIT"
+ },
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -700,6 +1913,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -715,6 +1934,78 @@
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
"license": "MIT"
},
+ "node_modules/pdfkit": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.19.1.tgz",
+ "integrity": "sha512-6Gzk+wDwTs4VSxsR5rCMTnIl5nlmkye1oWB0l2hDB1EX6ZNSIBroKQEv+2+fPPn+stVjyqzmsqRJVDfB9fo5DA==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/ciphers": "^1.0.0",
+ "@noble/hashes": "^1.6.0",
+ "fontkit": "^2.0.4",
+ "js-md5": "^0.8.3",
+ "linebreak": "^1.1.0",
+ "png-js": "^1.1.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.61.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz",
+ "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.61.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.61.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz",
+ "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/png-js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz",
+ "integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==",
+ "dependencies": {
+ "browserify-zlib": "^0.2.0"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -770,6 +2061,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/restructure": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
+ "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
+ "license": "MIT"
+ },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -832,6 +2129,15 @@
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
"license": "ISC"
},
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/sql-escaper": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
@@ -865,6 +2171,49 @@
"node": ">= 0.6"
}
},
+ "node_modules/tailwindcss": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
+ "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tiny-inflate": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -897,6 +2246,32 @@
"license": "MIT",
"peer": true
},
+ "node_modules/unicode-properties": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
+ "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "unicode-trie": "^2.0.0"
+ }
+ },
+ "node_modules/unicode-trie": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
+ "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^0.2.5",
+ "tiny-inflate": "^1.0.0"
+ }
+ },
+ "node_modules/unicode-trie/node_modules/pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
+ "license": "MIT"
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
diff --git a/package.json b/package.json
index bf3659a5..0741ba1e 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,8 @@
"dev": "nodemon ./bin/www"
},
"dependencies": {
+ "@fortawesome/fontawesome-free": "^7.2.0",
+ "@tailwindcss/cli": "^4.3.1",
"bcryptjs": "^3.0.3",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
@@ -17,6 +19,11 @@
"express-session": "^1.19.0",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
- "mysql2": "^3.22.3"
+ "mysql2": "^3.22.3",
+ "pdfkit": "^0.19.1",
+ "tailwindcss": "^4.3.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.61.0"
}
}
diff --git a/playwright-report/data/0602859b2b25aefea76d3938b46edf80928d21a3.md b/playwright-report/data/0602859b2b25aefea76d3938b46edf80928d21a3.md
new file mode 100644
index 00000000..1bef5abe
--- /dev/null
+++ b/playwright-report/data/0602859b2b25aefea76d3938b46edf80928d21a3.md
@@ -0,0 +1,132 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: assignment-update.spec.js >> Halaman Edit Assignment
+- Location: testing\assignment-update.spec.js:3:1
+
+# Error details
+
+```
+Test timeout of 30000ms exceeded.
+```
+
+```
+Error: locator.click: Test timeout of 30000ms exceeded.
+Call log:
+ - waiting for locator('.action.edit').first()
+
+```
+
+# Page snapshot
+
+```yaml
+- generic [ref=e5]:
+ - generic [ref=e6]:
+ - generic [ref=e7]:
+ - img "Facultyware Logo" [ref=e8]
+ - generic [ref=e9]:
+ - heading "Facultyware" [level=2] [ref=e10]
+ - text: Survey Management System
+ - generic [ref=e11]:
+ - link "🏠 Dashboard" [ref=e12] [cursor=pointer]:
+ - /url: /home
+ - link "📋 Survey" [ref=e13] [cursor=pointer]:
+ - /url: /survey
+ - link "❓ Question" [ref=e14] [cursor=pointer]:
+ - /url: /question
+ - link "🔗 Assignment" [ref=e15] [cursor=pointer]:
+ - /url: /assignment
+ - generic [ref=e16]:
+ - generic [ref=e17]: Facultyware v2.0
+ - link "🚪 Logout" [ref=e18] [cursor=pointer]:
+ - /url: /logout
+ - generic [ref=e19]:
+ - generic [ref=e20]:
+ - generic [ref=e21]:
+ - generic [ref=e22]: Welcome Back, hanif 👋
+ - paragraph [ref=e23]: Faculty Survey Management System
+ - generic [ref=e24]:
+ - generic [ref=e28]: System Online
+ - generic [ref=e29]:
+ - generic [ref=e30]: H
+ - generic [ref=e31]:
+ - strong [ref=e32]: hanif
+ - text: Administrator
+ - generic [ref=e33]:
+ - generic [ref=e34]:
+ - text: Survey Assignment
+ - heading "Assignment Management" [level=1] [ref=e35]
+ - paragraph [ref=e36]: Connect surveys with their questions.
+ - link "➕ New Assignment" [ref=e38] [cursor=pointer]:
+ - /url: /assignment/create
+ - generic [ref=e39]:
+ - generic [ref=e40] [cursor=pointer]:
+ - generic [ref=e41]: 🔗
+ - generic [ref=e42]:
+ - paragraph [ref=e43]: Total Assignment
+ - heading "0" [level=2] [ref=e44]
+ - generic [ref=e45] [cursor=pointer]:
+ - generic [ref=e46]: 📄
+ - generic [ref=e47]:
+ - paragraph [ref=e48]: Current Page
+ - heading "1" [level=2] [ref=e49]
+ - generic [ref=e50] [cursor=pointer]:
+ - generic [ref=e51]: 📚
+ - generic [ref=e52]:
+ - paragraph [ref=e53]: Total Page
+ - heading "0" [level=2] [ref=e54]
+ - generic [ref=e55] [cursor=pointer]:
+ - generic [ref=e56]: 🔍
+ - generic [ref=e57]:
+ - paragraph [ref=e58]: Search
+ - heading "-" [level=2] [ref=e59]
+ - generic [ref=e61]:
+ - textbox "Search survey or question..." [ref=e62]
+ - button "Search" [ref=e63]
+ - link "Reset" [ref=e64] [cursor=pointer]:
+ - /url: /assignment
+ - table [ref=e66]:
+ - rowgroup [ref=e67]:
+ - row "No Survey Question Order Action" [ref=e68]:
+ - columnheader "No" [ref=e69]
+ - columnheader "Survey" [ref=e70]
+ - columnheader "Question" [ref=e71]
+ - columnheader "Order" [ref=e72]
+ - columnheader "Action" [ref=e73]
+ - rowgroup [ref=e74]:
+ - row "📭 No Assignment No assignment available." [ref=e75]:
+ - cell "📭 No Assignment No assignment available." [ref=e76]:
+ - generic [ref=e77]:
+ - heading "📭 No Assignment" [level=2] [ref=e78]
+ - paragraph [ref=e79]: No assignment available.
+ - generic [ref=e81]: Page 1 of 0
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Halaman Edit Assignment', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/assignment');
+ 13 |
+> 14 | await page.locator(".action.edit").first().click();
+ | ^ Error: locator.click: Test timeout of 30000ms exceeded.
+ 15 |
+ 16 | await expect(page.locator('body')).toContainText('Edit Assignment');
+ 17 |
+ 18 | });
+```
\ No newline at end of file
diff --git a/playwright-report/data/322311203a8d6eae0636faae6d6d106a9e9343fb.md b/playwright-report/data/322311203a8d6eae0636faae6d6d106a9e9343fb.md
new file mode 100644
index 00000000..63176d61
--- /dev/null
+++ b/playwright-report/data/322311203a8d6eae0636faae6d6d106a9e9343fb.md
@@ -0,0 +1,106 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: assignment-delete.spec.js >> Tombol Hapus Assignment tampil
+- Location: testing\assignment-delete.spec.js:3:1
+
+# Error details
+
+```
+Error: expect(locator).toBeVisible() failed
+
+Locator: locator('.action.delete').first()
+Expected: visible
+Timeout: 5000ms
+Error: element(s) not found
+
+Call log:
+ - Expect "toBeVisible" with timeout 5000ms
+ - waiting for locator('.action.delete').first()
+
+```
+
+```yaml
+- img "Facultyware Logo"
+- heading "Facultyware" [level=2]
+- text: Survey Management System
+- link "🏠 Dashboard":
+ - /url: /home
+- link "📋 Survey":
+ - /url: /survey
+- link "❓ Question":
+ - /url: /question
+- link "🔗 Assignment":
+ - /url: /assignment
+- text: Facultyware v2.0
+- link "🚪 Logout":
+ - /url: /logout
+- text: Welcome Back, hanif 👋
+- paragraph: Faculty Survey Management System
+- text: System Online H
+- strong: hanif
+- text: Administrator Survey Assignment
+- heading "Assignment Management" [level=1]
+- paragraph: Connect surveys with their questions.
+- link "➕ New Assignment":
+ - /url: /assignment/create
+- text: 🔗
+- paragraph: Total Assignment
+- heading "0" [level=2]
+- text: 📄
+- paragraph: Current Page
+- heading "1" [level=2]
+- text: 📚
+- paragraph: Total Page
+- heading "0" [level=2]
+- text: 🔍
+- paragraph: Search
+- heading "-" [level=2]
+- textbox "Search survey or question..."
+- button "Search"
+- link "Reset":
+ - /url: /assignment
+- table:
+ - rowgroup:
+ - row "No Survey Question Order Action":
+ - columnheader "No"
+ - columnheader "Survey"
+ - columnheader "Question"
+ - columnheader "Order"
+ - columnheader "Action"
+ - rowgroup:
+ - row "📭 No Assignment No assignment available.":
+ - cell "📭 No Assignment No assignment available.":
+ - heading "📭 No Assignment" [level=2]
+ - paragraph: No assignment available.
+- text: Page 1 of 0
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Tombol Hapus Assignment tampil', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/assignment');
+ 13 |
+ 14 | await expect(
+ 15 | page.locator(".action.delete").first()
+> 16 | ).toBeVisible();
+ | ^ Error: expect(locator).toBeVisible() failed
+ 17 |
+ 18 | });
+```
\ No newline at end of file
diff --git a/playwright-report/data/4eb8eb57c91b44c7b79c32e081a63c23a95c39dd.md b/playwright-report/data/4eb8eb57c91b44c7b79c32e081a63c23a95c39dd.md
new file mode 100644
index 00000000..5efee2a1
--- /dev/null
+++ b/playwright-report/data/4eb8eb57c91b44c7b79c32e081a63c23a95c39dd.md
@@ -0,0 +1,59 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: question-create.spec.js >> Tambah Pertanyaan
+- Location: testing\question-create.spec.js:3:1
+
+# Error details
+
+```
+Error: expect(page).toHaveURL(expected) failed
+
+Expected pattern: /question\/survey\/1/
+Received string: "http://localhost:3000/question/create/1"
+Timeout: 5000ms
+
+Call log:
+ - Expect "toHaveURL" with timeout 5000ms
+ 13 × unexpected value "http://localhost:3000/question/create/1"
+
+```
+
+```yaml
+- 'heading "Cannot add or update a child row: a foreign key constraint fails (`facultyware`.`survey_question_assignments`, CONSTRAINT `survey_question_assignments_survey_id_foreign` FOREIGN KEY (`survey_id`) REFERENCES `surveys` (`id`))" [level=1]'
+- heading [level=2]
+- text: "Error: Cannot add or update a child row: a foreign key constraint fails (`facultyware`.`survey_question_assignments`, CONSTRAINT `survey_question_assignments_survey_id_foreign` FOREIGN KEY (`survey_id`) REFERENCES `surveys` (`id`)) at store (D:\\information system\\PWEB\\a12 pweb backup\\facultyware\\controllers\\questionController.js:161:18) at process.processTicksAndRejections (node:internal/process/task_queues:104:5)"
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Tambah Pertanyaan', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/question/create/1');
+ 13 |
+ 14 | await page.fill('textarea[name="question_text"]', 'Pertanyaan Playwright');
+ 15 |
+ 16 | await page.selectOption('select[name="type"]', 'text');
+ 17 |
+ 18 | await page.click('button[type="submit"]');
+ 19 |
+> 20 | await expect(page).toHaveURL(/question\/survey\/1/);
+ | ^ Error: expect(page).toHaveURL(expected) failed
+ 21 |
+ 22 | });
+```
\ No newline at end of file
diff --git a/playwright-report/data/9d42585cd24a4f9294d01b0ab27e14ec28274d99.md b/playwright-report/data/9d42585cd24a4f9294d01b0ab27e14ec28274d99.md
new file mode 100644
index 00000000..06b36f61
--- /dev/null
+++ b/playwright-report/data/9d42585cd24a4f9294d01b0ab27e14ec28274d99.md
@@ -0,0 +1,61 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: question.spec.js >> Membuka halaman Pertanyaan
+- Location: testing\question.spec.js:3:1
+
+# Error details
+
+```
+Error: expect(locator).toContainText(expected) failed
+
+Locator: locator('h1')
+Timeout: 5000ms
+- Expected substring - 1
++ Received string + 3
+
+- Question
++
++ Data Survey
++
+
+Call log:
+ - Expect "toContainText" with timeout 5000ms
+ - waiting for locator('h1')
+ 13 × locator resolved to
↵ Data Survey↵
+ - unexpected value "
+ Data Survey
+ "
+
+```
+
+```yaml
+- heading "Data Survey" [level=1]
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Membuka halaman Pertanyaan', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/question/survey/1');
+ 13 |
+> 14 | await expect(page.locator('h1')).toContainText('Question');
+ | ^ Error: expect(locator).toContainText(expected) failed
+ 15 |
+ 16 | });
+```
\ No newline at end of file
diff --git a/playwright-report/data/b5275161a1ac778678101ba33dac239aac6e992c.md b/playwright-report/data/b5275161a1ac778678101ba33dac239aac6e992c.md
new file mode 100644
index 00000000..f9047f30
--- /dev/null
+++ b/playwright-report/data/b5275161a1ac778678101ba33dac239aac6e992c.md
@@ -0,0 +1,141 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: question-delete.spec.js >> Tombol Hapus Pertanyaan tampil
+- Location: testing\question-delete.spec.js:3:1
+
+# Error details
+
+```
+Error: expect(locator).toBeVisible() failed
+
+Locator: locator('button[title="Delete Question"]').first()
+Expected: visible
+Timeout: 5000ms
+Error: element(s) not found
+
+Call log:
+ - Expect "toBeVisible" with timeout 5000ms
+ - waiting for locator('button[title="Delete Question"]').first()
+
+```
+
+```yaml
+- img "Facultyware Logo"
+- heading "Facultyware" [level=2]
+- text: Survey Management System
+- link "🏠 Dashboard":
+ - /url: /home
+- link "📋 Survey":
+ - /url: /survey
+- link "❓ Question":
+ - /url: /question
+- link "🔗 Assignment":
+ - /url: /assignment
+- text: Facultyware v2.0
+- link "🚪 Logout":
+ - /url: /logout
+- text: Welcome Back, hanif 👋
+- paragraph: Faculty Survey Management System
+- text: System Online H
+- strong: hanif
+- text: Administrator Survey Management
+- heading "Data Survey" [level=1]
+- paragraph: Create, manage and publish faculty surveys professionally.
+- link "📄 Export PDF":
+ - /url: /survey/export/pdf
+- link "➕ New Survey":
+ - /url: /survey/create
+- text: 📋
+- paragraph: Total Survey
+- heading "2" [level=2]
+- text: 📄
+- paragraph: Current Page
+- heading "1" [level=2]
+- text: 📚
+- paragraph: Total Page
+- heading "1" [level=2]
+- text: 🔍
+- paragraph: Keyword
+- heading "-" [level=2]
+- textbox "Search survey..."
+- button "Search"
+- link "Reset":
+ - /url: /survey
+- table:
+ - rowgroup:
+ - row "No Survey Question Start End Status Action":
+ - columnheader "No"
+ - columnheader "Survey"
+ - columnheader "Question"
+ - columnheader "Start"
+ - columnheader "End"
+ - columnheader "Status"
+ - columnheader "Action"
+ - rowgroup:
+ - row "1 Survey Playwright Faculty Survey 0 Question 23/6/2026 30/6/2026 🔴 Inactive 📋 🚀 ✏️ 🗑️":
+ - cell "1"
+ - cell "Survey Playwright Faculty Survey":
+ - heading "Survey Playwright" [level=3]
+ - text: Faculty Survey
+ - cell "0 Question"
+ - cell "23/6/2026"
+ - cell "30/6/2026"
+ - cell "🔴 Inactive"
+ - cell "📋 🚀 ✏️ 🗑️":
+ - link "📋":
+ - /url: /question/survey/25
+ - button "🚀"
+ - link "✏️":
+ - /url: /survey/edit/25
+ - button "🗑️"
+ - row "2 trhwth Faculty Survey 0 Question 25/6/2026 26/6/2026 🔴 Inactive 📋 🚀 ✏️ 🗑️":
+ - cell "2"
+ - cell "trhwth Faculty Survey":
+ - heading "trhwth" [level=3]
+ - text: Faculty Survey
+ - cell "0 Question"
+ - cell "25/6/2026"
+ - cell "26/6/2026"
+ - cell "🔴 Inactive"
+ - cell "📋 🚀 ✏️ 🗑️":
+ - link "📋":
+ - /url: /question/survey/24
+ - button "🚀"
+ - link "✏️":
+ - /url: /survey/edit/24
+ - button "🗑️"
+- text: Page
+- strong: "1"
+- text: of
+- strong: "1"
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Tombol Hapus Pertanyaan tampil', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/question/survey/1');
+ 13 |
+ 14 | await expect(
+ 15 | page.locator('button[title="Delete Question"]').first()
+> 16 | ).toBeVisible();
+ | ^ Error: expect(locator).toBeVisible() failed
+ 17 |
+ 18 | });
+```
\ No newline at end of file
diff --git a/playwright-report/data/de017f1155a89e270bfd97671ac03f253b362379.md b/playwright-report/data/de017f1155a89e270bfd97671ac03f253b362379.md
new file mode 100644
index 00000000..67add298
--- /dev/null
+++ b/playwright-report/data/de017f1155a89e270bfd97671ac03f253b362379.md
@@ -0,0 +1,174 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: question-update.spec.js >> Halaman Edit Pertanyaan
+- Location: testing\question-update.spec.js:3:1
+
+# Error details
+
+```
+Test timeout of 30000ms exceeded.
+```
+
+```
+Error: locator.click: Test timeout of 30000ms exceeded.
+Call log:
+ - waiting for locator('a[title="Edit Question"]').first()
+
+```
+
+# Page snapshot
+
+```yaml
+- generic [ref=e5]:
+ - generic [ref=e6]:
+ - generic [ref=e7]:
+ - img "Facultyware Logo" [ref=e8]
+ - generic [ref=e9]:
+ - heading "Facultyware" [level=2] [ref=e10]
+ - text: Survey Management System
+ - generic [ref=e11]:
+ - link "🏠 Dashboard" [ref=e12] [cursor=pointer]:
+ - /url: /home
+ - link "📋 Survey" [ref=e13] [cursor=pointer]:
+ - /url: /survey
+ - link "❓ Question" [ref=e14] [cursor=pointer]:
+ - /url: /question
+ - link "🔗 Assignment" [ref=e15] [cursor=pointer]:
+ - /url: /assignment
+ - generic [ref=e16]:
+ - generic [ref=e17]: Facultyware v2.0
+ - link "🚪 Logout" [ref=e18] [cursor=pointer]:
+ - /url: /logout
+ - generic [ref=e19]:
+ - generic [ref=e20]:
+ - generic [ref=e21]:
+ - generic [ref=e22]: Welcome Back, hanif 👋
+ - paragraph [ref=e23]: Faculty Survey Management System
+ - generic [ref=e24]:
+ - generic [ref=e28]: System Online
+ - generic [ref=e29]:
+ - generic [ref=e30]: H
+ - generic [ref=e31]:
+ - strong [ref=e32]: hanif
+ - text: Administrator
+ - generic [ref=e33]:
+ - generic [ref=e34]:
+ - text: Survey Management
+ - heading "Data Survey" [level=1] [ref=e35]
+ - paragraph [ref=e36]: Create, manage and publish faculty surveys professionally.
+ - generic [ref=e37]:
+ - link "📄 Export PDF" [ref=e38] [cursor=pointer]:
+ - /url: /survey/export/pdf
+ - link "➕ New Survey" [ref=e39] [cursor=pointer]:
+ - /url: /survey/create
+ - generic [ref=e40]:
+ - generic [ref=e41] [cursor=pointer]:
+ - generic [ref=e42]: 📋
+ - generic [ref=e43]:
+ - paragraph [ref=e44]: Total Survey
+ - heading "2" [level=2] [ref=e45]
+ - generic [ref=e46] [cursor=pointer]:
+ - generic [ref=e47]: 📄
+ - generic [ref=e48]:
+ - paragraph [ref=e49]: Current Page
+ - heading "1" [level=2] [ref=e50]
+ - generic [ref=e51] [cursor=pointer]:
+ - generic [ref=e52]: 📚
+ - generic [ref=e53]:
+ - paragraph [ref=e54]: Total Page
+ - heading "1" [level=2] [ref=e55]
+ - generic [ref=e56] [cursor=pointer]:
+ - generic [ref=e57]: 🔍
+ - generic [ref=e58]:
+ - paragraph [ref=e59]: Keyword
+ - heading "-" [level=2] [ref=e60]
+ - generic [ref=e62]:
+ - textbox "Search survey..." [ref=e63]
+ - button "Search" [ref=e64]
+ - link "Reset" [ref=e65] [cursor=pointer]:
+ - /url: /survey
+ - table [ref=e67]:
+ - rowgroup [ref=e68]:
+ - row "No Survey Question Start End Status Action" [ref=e69]:
+ - columnheader "No" [ref=e70]
+ - columnheader "Survey" [ref=e71]
+ - columnheader "Question" [ref=e72]
+ - columnheader "Start" [ref=e73]
+ - columnheader "End" [ref=e74]
+ - columnheader "Status" [ref=e75]
+ - columnheader "Action" [ref=e76]
+ - rowgroup [ref=e77]:
+ - row "1 Survey Playwright Faculty Survey 0 Question 23/6/2026 30/6/2026 🔴 Inactive 📋 🚀 ✏️ 🗑️" [ref=e78]:
+ - cell "1" [ref=e79]
+ - cell "Survey Playwright Faculty Survey" [ref=e80]:
+ - generic [ref=e81]:
+ - heading "Survey Playwright" [level=3] [ref=e82]
+ - text: Faculty Survey
+ - cell "0 Question" [ref=e83]
+ - cell "23/6/2026" [ref=e84]
+ - cell "30/6/2026" [ref=e85]
+ - cell "🔴 Inactive" [ref=e86]:
+ - generic [ref=e87]: 🔴 Inactive
+ - cell "📋 🚀 ✏️ 🗑️" [ref=e88]:
+ - generic [ref=e89]:
+ - link "📋" [ref=e90] [cursor=pointer]:
+ - /url: /question/survey/25
+ - button "🚀" [ref=e92] [cursor=pointer]
+ - link "✏️" [ref=e93] [cursor=pointer]:
+ - /url: /survey/edit/25
+ - button "🗑️" [ref=e95] [cursor=pointer]
+ - row "2 trhwth Faculty Survey 0 Question 25/6/2026 26/6/2026 🔴 Inactive 📋 🚀 ✏️ 🗑️" [ref=e96]:
+ - cell "2" [ref=e97]
+ - cell "trhwth Faculty Survey" [ref=e98]:
+ - generic [ref=e99]:
+ - heading "trhwth" [level=3] [ref=e100]
+ - text: Faculty Survey
+ - cell "0 Question" [ref=e101]
+ - cell "25/6/2026" [ref=e102]
+ - cell "26/6/2026" [ref=e103]
+ - cell "🔴 Inactive" [ref=e104]:
+ - generic [ref=e105]: 🔴 Inactive
+ - cell "📋 🚀 ✏️ 🗑️" [ref=e106]:
+ - generic [ref=e107]:
+ - link "📋" [ref=e108] [cursor=pointer]:
+ - /url: /question/survey/24
+ - button "🚀" [ref=e110] [cursor=pointer]
+ - link "✏️" [ref=e111] [cursor=pointer]:
+ - /url: /survey/edit/24
+ - button "🗑️" [ref=e113] [cursor=pointer]
+ - generic [ref=e115]:
+ - text: Page
+ - strong [ref=e116]: "1"
+ - text: of
+ - strong [ref=e117]: "1"
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Halaman Edit Pertanyaan', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/question/survey/1');
+ 13 |
+> 14 | await page.locator('a[title="Edit Question"]').first().click();
+ | ^ Error: locator.click: Test timeout of 30000ms exceeded.
+ 15 |
+ 16 | await expect(page.locator('h1')).toContainText('Edit');
+ 17 |
+ 18 | });
+```
\ No newline at end of file
diff --git a/playwright-report/data/f8a33940c014a9aea72fc2f9d62d406f120b5e15.md b/playwright-report/data/f8a33940c014a9aea72fc2f9d62d406f120b5e15.md
new file mode 100644
index 00000000..6c4a30bb
--- /dev/null
+++ b/playwright-report/data/f8a33940c014a9aea72fc2f9d62d406f120b5e15.md
@@ -0,0 +1,142 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: assignment-create.spec.js >> Tambah Assignment
+- Location: testing\assignment-create.spec.js:3:1
+
+# Error details
+
+```
+Test timeout of 30000ms exceeded.
+```
+
+```
+Error: page.selectOption: Test timeout of 30000ms exceeded.
+Call log:
+ - waiting for locator('select[name="survey_id"]')
+ - locator resolved to
+ - attempting select option action
+ 2 × waiting for element to be visible and enabled
+ - did not find some options
+ - retrying select option action
+ - waiting 20ms
+ 2 × waiting for element to be visible and enabled
+ - did not find some options
+ - retrying select option action
+ - waiting 100ms
+ 54 × waiting for element to be visible and enabled
+ - did not find some options
+ - retrying select option action
+ - waiting 500ms
+
+```
+
+# Page snapshot
+
+```yaml
+- generic [ref=e5]:
+ - generic [ref=e6]:
+ - generic [ref=e7]:
+ - img "Facultyware Logo" [ref=e8]
+ - generic [ref=e9]:
+ - heading "Facultyware" [level=2] [ref=e10]
+ - text: Survey Management System
+ - generic [ref=e11]:
+ - link "🏠 Dashboard" [ref=e12] [cursor=pointer]:
+ - /url: /home
+ - link "📋 Survey" [ref=e13] [cursor=pointer]:
+ - /url: /survey
+ - link "❓ Question" [ref=e14] [cursor=pointer]:
+ - /url: /question
+ - link "🔗 Assignment" [ref=e15] [cursor=pointer]:
+ - /url: /assignment
+ - generic [ref=e16]:
+ - generic [ref=e17]: Facultyware v2.0
+ - link "🚪 Logout" [ref=e18] [cursor=pointer]:
+ - /url: /logout
+ - generic [ref=e19]:
+ - generic [ref=e20]:
+ - generic [ref=e21]:
+ - generic [ref=e22]: Welcome Back, hanif 👋
+ - paragraph [ref=e23]: Faculty Survey Management System
+ - generic [ref=e24]:
+ - generic [ref=e28]: System Online
+ - generic [ref=e29]:
+ - generic [ref=e30]: H
+ - generic [ref=e31]:
+ - strong [ref=e32]: hanif
+ - text: Administrator
+ - generic [ref=e33]:
+ - generic [ref=e34]:
+ - text: Survey Assignment
+ - heading "Create Assignment" [level=1] [ref=e35]
+ - paragraph [ref=e36]: Assign a question into a survey.
+ - link "← Back" [ref=e37] [cursor=pointer]:
+ - /url: /assignment
+ - generic [ref=e39]:
+ - generic [ref=e40]:
+ - generic [ref=e41]: Survey
+ - combobox [ref=e42]:
+ - option "trhwth" [selected]
+ - generic [ref=e43]:
+ - generic [ref=e44]: Question
+ - combobox [ref=e45]:
+ - option "Bagaimana fasilitas ruang kelas?" [selected]
+ - option "Bagaimana keamanan lingkungan kampus?"
+ - option "Bagaimana kebersihan lingkungan kampus?"
+ - option "Bagaimana kedisiplinan dosen dalam mengajar?"
+ - option "Bagaimana kualitas jaringan internet kampus?"
+ - option "Bagaimana kualitas materi perkuliahan?"
+ - option "Bagaimana kualitas pelayanan dosen?"
+ - option "Bagaimana pelayanan administrasi akademik?"
+ - option "Bagaimana pelayanan perpustakaan?"
+ - option "Seberapa puas Anda terhadap layanan kampus secara keseluruhan?"
+ - generic [ref=e46]:
+ - generic [ref=e47]: Order
+ - spinbutton [ref=e48]: "1"
+ - button "💾 Save Assignment" [ref=e49]
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Tambah Assignment', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]','hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]','hanif123');
+ 9 |
+ 10 | await page.getByRole('button').click();
+ 11 |
+ 12 | await page.goto('http://localhost:3000/assignment/create');
+ 13 |
+> 14 | await page.selectOption(
+ | ^ Error: page.selectOption: Test timeout of 30000ms exceeded.
+ 15 | 'select[name="survey_id"]',
+ 16 | { index: 1 }
+ 17 | );
+ 18 |
+ 19 | await page.selectOption(
+ 20 | 'select[name="survey_question_id"]',
+ 21 | { index: 1 }
+ 22 | );
+ 23 |
+ 24 | await page.fill(
+ 25 | 'input[name="order"]',
+ 26 | '99'
+ 27 | );
+ 28 |
+ 29 | await page.locator('button.hero-btn').click();
+ 30 |
+ 31 | await expect(page).toHaveURL(/assignment/);
+ 32 |
+ 33 | });
+```
\ No newline at end of file
diff --git a/playwright-report/index.html b/playwright-report/index.html
new file mode 100644
index 00000000..efe66ee7
--- /dev/null
+++ b/playwright-report/index.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+data:application/zip;base64,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
\ No newline at end of file
diff --git a/playwright.config.js b/playwright.config.js
new file mode 100644
index 00000000..23add221
--- /dev/null
+++ b/playwright.config.js
@@ -0,0 +1,18 @@
+const { defineConfig } = require('@playwright/test');
+
+module.exports = defineConfig({
+ testDir: './testing',
+
+ timeout: 30000,
+
+ use: {
+ browserName: 'chromium',
+ headless: true,
+ baseURL: 'http://localhost:3000'
+ },
+
+ reporter: [
+ ['list'],
+ ['html']
+ ]
+});
\ No newline at end of file
diff --git a/public/assets/dashboard.css b/public/assets/dashboard.css
new file mode 100644
index 00000000..d5d4c328
--- /dev/null
+++ b/public/assets/dashboard.css
@@ -0,0 +1,1195 @@
+/* ===========================================
+FACULTYWARE v2 DASHBOARD
+=========================================== */
+
+*{
+margin:0;
+padding:0;
+box-sizing:border-box;
+}
+
+body{
+
+font-family:Inter,Segoe UI,sans-serif;
+
+background:#08111f;
+
+color:#fff;
+
+overflow-x:hidden;
+
+}
+
+/* BACKGROUND */
+
+.dashboard-bg{
+ position: fixed;
+ inset: 0;
+ background: rgba(8,17,31,.70);
+ z-index: -3;
+}
+
+.dashboard-blur{
+
+position:fixed;
+
+width:650px;
+
+height:650px;
+
+background:#4F46E5;
+
+filter:blur(180px);
+
+opacity:.18;
+
+top:-220px;
+
+right:-180px;
+
+z-index:-2;
+
+animation:moveGlow 10s infinite alternate;
+
+}
+
+@keyframes moveGlow{
+
+from{
+
+transform:translateY(0);
+
+}
+
+to{
+
+transform:translateY(80px);
+
+}
+
+}
+
+/* ================================= */
+
+.wrapper{
+
+display:flex;
+
+min-height:100vh;
+
+}
+
+/* SIDEBAR */
+
+.sidebar{
+
+width:260px;
+
+background:rgba(255,255,255,.05);
+
+backdrop-filter:blur(22px);
+
+border-right:1px solid rgba(255,255,255,.08);
+
+padding:28px;
+
+display:flex;
+
+flex-direction:column;
+
+}
+
+.logo{
+
+display:flex;
+
+align-items:center;
+
+gap:15px;
+
+margin-bottom:45px;
+
+}
+
+.logo img{
+
+width:52px;
+
+height:52px;
+
+border-radius:14px;
+
+}
+
+.logo h2{
+
+font-size:24px;
+
+font-weight:700;
+
+}
+
+.menu{
+
+display:flex;
+
+flex-direction:column;
+
+gap:12px;
+
+}
+
+.menu a{
+
+color:white;
+
+text-decoration:none;
+
+padding:14px 18px;
+
+border-radius:14px;
+
+transition:.3s;
+
+font-weight:500;
+
+}
+
+.menu a:hover{
+
+background:#4F46E5;
+
+transform:translateX(8px);
+
+}
+
+/* CONTENT */
+
+.content{
+
+flex:1;
+
+padding:40px;
+
+}
+
+/* NAVBAR */
+
+.navbar{
+
+display:flex;
+
+justify-content:space-between;
+
+align-items:center;
+
+margin-bottom:35px;
+
+}
+
+.page-title{
+
+font-size:34px;
+
+font-weight:700;
+
+}
+
+.page-subtitle{
+
+margin-top:6px;
+
+opacity:.65;
+
+}
+
+.navbar-right{
+
+display:flex;
+
+align-items:center;
+
+gap:20px;
+
+}
+
+.status{
+
+display:flex;
+
+align-items:center;
+
+gap:10px;
+
+padding:12px 18px;
+
+border-radius:999px;
+
+background:rgba(255,255,255,.05);
+
+}
+
+.dot{
+
+width:10px;
+
+height:10px;
+
+background:#22C55E;
+
+border-radius:50%;
+
+box-shadow:0 0 12px #22C55E;
+
+}
+
+.user-box{
+
+display:flex;
+
+align-items:center;
+
+gap:15px;
+
+padding:10px 18px;
+
+border-radius:18px;
+
+background:rgba(255,255,255,.05);
+
+}
+
+.avatar{
+
+width:48px;
+
+height:48px;
+
+border-radius:50%;
+
+background:#4F46E5;
+
+display:flex;
+
+align-items:center;
+
+justify-content:center;
+
+font-size:20px;
+
+font-weight:bold;
+
+}
+
+/* HERO */
+
+.hero{
+
+display:flex;
+
+justify-content:space-between;
+
+align-items:center;
+
+padding:45px;
+
+border-radius:24px;
+
+background:rgba(255,255,255,.05);
+
+backdrop-filter:blur(20px);
+
+border:1px solid rgba(255,255,255,.08);
+
+margin-bottom:35px;
+
+}
+
+.hero-badge{
+
+background:#4F46E5;
+
+padding:8px 16px;
+
+border-radius:999px;
+
+font-size:13px;
+
+}
+
+.hero-title{
+
+font-size:44px;
+
+margin-top:20px;
+
+}
+
+.hero-description{
+
+margin-top:18px;
+
+max-width:520px;
+
+line-height:1.8;
+
+opacity:.75;
+
+}
+
+.hero-buttons{
+
+margin-top:28px;
+
+display:flex;
+
+gap:15px;
+
+}
+
+.hero-btn{
+
+background:#4F46E5;
+
+padding:14px 24px;
+
+border-radius:14px;
+
+color:white;
+
+text-decoration:none;
+
+transition:.3s;
+
+}
+
+.hero-btn:hover{
+
+transform:translateY(-4px);
+
+}
+
+.hero-btn-outline{
+
+border:1px solid rgba(255,255,255,.15);
+
+padding:14px 24px;
+
+border-radius:14px;
+
+color:white;
+
+text-decoration:none;
+
+}
+
+.hero-logo{
+
+width:180px;
+
+animation:float 5s infinite;
+
+}
+
+@keyframes float{
+
+50%{
+
+transform:translateY(-12px);
+
+}
+
+}
+
+/* STATS */
+
+.stats-grid{
+
+display:grid;
+
+grid-template-columns:repeat(4,1fr);
+
+gap:22px;
+
+margin-bottom:40px;
+
+}
+
+.stat-card{
+
+display:flex;
+
+gap:18px;
+
+align-items:center;
+
+padding:28px;
+
+border-radius:20px;
+
+background:rgba(255,255,255,.05);
+
+transition:.35s;
+
+}
+
+.stat-card:hover{
+
+background:#4F46E5;
+
+transform:translateY(-8px);
+
+}
+
+.stat-icon{
+
+font-size:40px;
+
+}
+
+.stat-card p{
+
+opacity:.7;
+
+margin-bottom:8px;
+
+}
+
+.stat-card h2{
+
+font-size:36px;
+
+}
+
+/* QUICK */
+
+.section-title{
+
+margin-bottom:20px;
+
+font-size:28px;
+
+}
+
+.quick-grid{
+
+display:grid;
+
+grid-template-columns:repeat(2,1fr);
+
+gap:20px;
+
+}
+
+.quick-grid a{
+
+text-decoration:none;
+
+}
+
+.quick-card{
+
+padding:28px;
+
+background:rgba(255,255,255,.05);
+
+border-radius:20px;
+
+color:white;
+
+transition:.3s;
+
+}
+
+.quick-card:hover{
+
+background:#4F46E5;
+
+transform:translateY(-8px);
+
+}
+
+.quick-card p{
+
+margin-top:10px;
+
+opacity:.7;
+
+}
+
+/* BOTTOM */
+
+.dashboard-bottom{
+
+display:grid;
+
+grid-template-columns:1fr 1fr;
+
+gap:22px;
+
+margin-top:35px;
+
+}
+
+.activity-card{
+
+background:rgba(255,255,255,.05);
+
+padding:30px;
+
+border-radius:20px;
+
+}
+
+.activity-card h2{
+
+margin-bottom:20px;
+
+}
+
+.activity-list{
+
+list-style:none;
+
+}
+
+.activity-list li{
+
+padding:14px 0;
+
+display:flex;
+
+gap:12px;
+
+align-items:center;
+
+border-bottom:1px solid rgba(255,255,255,.08);
+
+}
+
+.activity-dot{
+
+width:10px;
+
+height:10px;
+
+background:#4F46E5;
+
+border-radius:50%;
+
+}
+
+.progress-item{
+
+margin-bottom:22px;
+
+}
+
+.progress{
+
+height:10px;
+
+background:rgba(255,255,255,.08);
+
+border-radius:999px;
+
+overflow:hidden;
+
+margin-top:8px;
+
+}
+
+.progress-fill{
+
+height:100%;
+
+background:linear-gradient(90deg,#4F46E5,#06B6D4);
+
+animation:grow 1.5s;
+
+}
+
+.survey{width:85%;}
+.question{width:92%;}
+.option{width:74%;}
+.assignment{width:65%;}
+
+@keyframes grow{
+
+from{
+
+width:0;
+
+}
+
+}
+
+canvas{
+
+width:100%!important;
+
+}
+
+@media(max-width:1100px){
+
+.sidebar{
+
+width:90px;
+
+}
+
+.logo h2{
+
+display:none;
+
+}
+
+.menu a{
+
+font-size:0;
+
+}
+
+.menu a::before{
+
+font-size:20px;
+
+}
+
+.stats-grid{
+
+grid-template-columns:repeat(2,1fr);
+
+}
+
+.quick-grid{
+
+grid-template-columns:1fr;
+
+}
+
+.dashboard-bottom{
+
+grid-template-columns:1fr;
+
+}
+
+.hero{
+
+flex-direction:column;
+
+text-align:center;
+
+gap:30px;
+
+}
+
+}
+/* ACTIVE MENU */
+
+.menu a.active{
+
+background:#4F46E5;
+
+color:#fff;
+
+box-shadow:0 10px 30px rgba(79,70,229,.35);
+
+}
+
+/* ICON */
+
+.menu a span:first-child{
+
+width:26px;
+
+display:inline-flex;
+
+justify-content:center;
+
+margin-right:10px;
+
+}
+
+/* LOGO */
+
+.logo small{
+
+opacity:.65;
+
+display:block;
+
+margin-top:2px;
+
+font-size:12px;
+
+}
+
+/* LOGOUT */
+
+.logout-btn{
+
+display:block;
+
+padding:14px;
+
+text-align:center;
+
+background:#EF4444;
+
+color:#fff;
+
+text-decoration:none;
+
+border-radius:14px;
+
+font-weight:600;
+
+transition:.3s;
+
+}
+
+.logout-btn:hover{
+
+background:#DC2626;
+
+transform:translateY(-3px);
+
+}
+/* ==========================
+NAVBAR IMPROVEMENT
+========================== */
+
+.navbar{
+
+display:flex;
+
+justify-content:space-between;
+
+align-items:center;
+
+margin-bottom:35px;
+
+}
+
+.page-title{
+
+font-size:34px;
+
+font-weight:700;
+
+}
+
+.page-subtitle{
+
+margin-top:6px;
+
+opacity:.7;
+
+}
+
+.navbar-right{
+
+display:flex;
+
+align-items:center;
+
+gap:20px;
+
+}
+
+.status{
+
+display:flex;
+
+align-items:center;
+
+gap:10px;
+
+padding:10px 18px;
+
+border-radius:999px;
+
+background:rgba(255,255,255,.06);
+
+border:1px solid rgba(255,255,255,.08);
+
+}
+
+.user-box{
+
+display:flex;
+
+align-items:center;
+
+gap:14px;
+
+padding:10px 16px;
+
+border-radius:18px;
+
+background:rgba(255,255,255,.06);
+
+border:1px solid rgba(255,255,255,.08);
+
+transition:.3s;
+
+}
+
+.user-box:hover{
+
+transform:translateY(-3px);
+
+background:rgba(79,70,229,.15);
+
+}
+
+.avatar{
+
+width:46px;
+
+height:46px;
+
+border-radius:50%;
+
+display:flex;
+
+justify-content:center;
+
+align-items:center;
+
+background:linear-gradient(135deg,#4F46E5,#06B6D4);
+
+font-size:18px;
+
+font-weight:bold;
+
+color:white;
+
+}
+/* =========================
+PREMIUM DASHBOARD V3
+=========================*/
+
+.content{
+ padding:40px;
+}
+
+.hero{
+ position:relative;
+ overflow:hidden;
+ background:rgba(255,255,255,.06);
+ border:1px solid rgba(255,255,255,.08);
+ box-shadow:
+ 0 20px 60px rgba(0,0,0,.35),
+ inset 0 1px rgba(255,255,255,.08);
+}
+
+.hero::after{
+ content:"";
+ position:absolute;
+ right:-80px;
+ top:-80px;
+ width:260px;
+ height:260px;
+ background:#4F46E5;
+ filter:blur(140px);
+ opacity:.35;
+}
+
+.hero-title{
+ font-size:48px;
+ line-height:1.2;
+}
+
+.hero-description{
+ font-size:17px;
+ color:#cbd5e1;
+}
+
+.hero-btn,
+.hero-btn-outline{
+ transition:.35s;
+}
+
+.hero-btn:hover{
+ transform:translateY(-5px);
+ box-shadow:0 18px 35px rgba(79,70,229,.45);
+}
+
+.hero-btn-outline:hover{
+ background:rgba(255,255,255,.08);
+}
+
+/* ========================= */
+
+.stat-card{
+ position:relative;
+ overflow:hidden;
+ cursor:pointer;
+}
+
+.stat-card::before{
+ content:"";
+ position:absolute;
+ top:0;
+ left:-100%;
+ width:100%;
+ height:100%;
+ background:
+ linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255,255,255,.12),
+ transparent
+ );
+ transition:.6s;
+}
+
+.stat-card:hover::before{
+ left:100%;
+}
+
+.stat-card:hover{
+ transform:translateY(-10px) scale(1.02);
+ box-shadow:
+ 0 25px 45px rgba(79,70,229,.35);
+}
+
+.stat-card h2{
+ font-size:42px;
+ margin-top:5px;
+}
+
+.stat-icon{
+ font-size:48px;
+}
+
+/* ========================= */
+
+.quick-card{
+ position:relative;
+ overflow:hidden;
+}
+
+.quick-card::after{
+ content:"→";
+ position:absolute;
+ right:25px;
+ top:22px;
+ font-size:28px;
+ opacity:0;
+ transition:.35s;
+}
+
+.quick-card:hover::after{
+ opacity:1;
+ right:18px;
+}
+
+.quick-card:hover{
+ box-shadow:
+ 0 18px 35px rgba(79,70,229,.3);
+}
+
+/* ========================= */
+
+.activity-card{
+ border:1px solid rgba(255,255,255,.08);
+ box-shadow:
+ 0 20px 40px rgba(0,0,0,.25);
+}
+
+.activity-card h2{
+ margin-bottom:25px;
+ font-size:22px;
+}
+
+.activity-list li{
+ transition:.25s;
+}
+
+.activity-list li:hover{
+ padding-left:12px;
+ color:#60A5FA;
+}
+
+.activity-dot{
+ box-shadow:
+ 0 0 12px #4F46E5;
+}
+
+/* ========================= */
+
+.progress{
+ height:12px;
+}
+
+.progress-fill{
+ border-radius:999px;
+ box-shadow:
+ 0 0 18px rgba(79,70,229,.45);
+}
+
+/* ========================= */
+
+.sidebar{
+ box-shadow:
+ 15px 0 35px rgba(0,0,0,.28);
+}
+
+.sidebar-menu a{
+ position:relative;
+}
+
+.sidebar-menu a::before{
+ content:"";
+ position:absolute;
+ left:-28px;
+ top:0;
+ width:5px;
+ height:100%;
+ border-radius:10px;
+ background:#4F46E5;
+ opacity:0;
+ transition:.3s;
+}
+
+.sidebar-menu a:hover::before,
+.sidebar-menu a.active::before{
+ opacity:1;
+}
+
+.sidebar-menu a:hover{
+ background:rgba(79,70,229,.18);
+}
+
+.sidebar-bottom a{
+ transition:.3s;
+}
+
+.sidebar-bottom a:hover{
+ color:#ff6b6b;
+}
+
+/* ========================= */
+
+.dashboard-bg{
+ animation:bgMove 15s linear infinite alternate;
+}
+
+@keyframes bgMove{
+
+0%{
+filter:hue-rotate(0deg);
+}
+
+100%{
+filter:hue-rotate(20deg);
+}
+
+}
+
+/* ========================= */
+
+.hero-logo{
+ width:210px;
+ filter:
+ drop-shadow(0 20px 40px rgba(79,70,229,.4));
+}
+
+/* ========================= */
+
+.section-title{
+ margin-top:45px;
+ margin-bottom:22px;
+}
+.dashboard-video{
+ position: fixed;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ z-index: -4;
+}
+.clock-box{
+
+padding:12px 18px;
+
+background:rgba(255,255,255,.06);
+
+border-radius:16px;
+
+border:1px solid rgba(255,255,255,.08);
+
+text-align:center;
+
+min-width:180px;
+
+}
+
+#clock{
+
+font-size:20px;
+
+font-weight:bold;
+
+letter-spacing:1px;
+
+}
+
+#date{
+
+opacity:.7;
+
+font-size:12px;
+
+}
+.action-group{
+ display:flex;
+ gap:10px;
+ justify-content:center;
+ align-items:center;
+}
+
+.action{
+ width:42px;
+ height:42px;
+ border:none;
+ border-radius:12px;
+ display:flex;
+ justify-content:center;
+ align-items:center;
+ color:#fff;
+ cursor:pointer;
+ text-decoration:none;
+ transition:.3s;
+ font-size:18px;
+}
+
+.action.option{
+ background:#06B6D4;
+}
+
+.action.option:hover{
+ background:#0891B2;
+ transform:translateY(-3px);
+}
+
+.action.edit{
+ background:#F59E0B;
+}
+
+.action.edit:hover{
+ background:#D97706;
+ transform:translateY(-3px);
+}
+
+.action.delete{
+ background:#EF4444;
+}
+
+.action.delete:hover{
+ background:#DC2626;
+ transform:translateY(-3px);
+}
\ No newline at end of file
diff --git a/public/assets/images/dashboard-bg.jpg b/public/assets/images/dashboard-bg.jpg
new file mode 100644
index 00000000..6c992dd9
Binary files /dev/null and b/public/assets/images/dashboard-bg.jpg differ
diff --git a/public/assets/images/logo.jpg b/public/assets/images/logo.jpg
new file mode 100644
index 00000000..6f6cf98a
Binary files /dev/null and b/public/assets/images/logo.jpg differ
diff --git a/public/assets/images/logo.png b/public/assets/images/logo.png
new file mode 100644
index 00000000..6f6cf98a
Binary files /dev/null and b/public/assets/images/logo.png differ
diff --git a/public/assets/images/side-login.jpg b/public/assets/images/side-login.jpg
new file mode 100644
index 00000000..df611b6c
Binary files /dev/null and b/public/assets/images/side-login.jpg differ
diff --git a/public/assets/question.css b/public/assets/question.css
new file mode 100644
index 00000000..6796e1bb
--- /dev/null
+++ b/public/assets/question.css
@@ -0,0 +1,13 @@
+@import url("/assets/survey.css");
+
+.page-header h1{
+ color:#fff;
+}
+
+.question-type{
+ padding:8px 14px;
+ border-radius:999px;
+ background:rgba(79,70,229,.15);
+ color:#8B5CF6;
+ font-weight:600;
+}
\ No newline at end of file
diff --git a/public/assets/sidebar.ejs b/public/assets/sidebar.ejs
new file mode 100644
index 00000000..898326a1
--- /dev/null
+++ b/public/assets/sidebar.ejs
@@ -0,0 +1,27 @@
+
\ No newline at end of file
diff --git a/public/assets/survey.css b/public/assets/survey.css
new file mode 100644
index 00000000..fd8f8aee
--- /dev/null
+++ b/public/assets/survey.css
@@ -0,0 +1,466 @@
+/* ==========================================
+SURVEY PAGE
+========================================== */
+
+.page-header{
+
+display:flex;
+
+justify-content:space-between;
+
+align-items:center;
+
+margin-bottom:35px;
+
+gap:30px;
+
+}
+
+.page-header h1{
+
+font-size:42px;
+
+margin:15px 0 10px;
+
+font-weight:700;
+
+}
+
+.page-header p{
+
+opacity:.75;
+
+font-size:16px;
+
+}
+
+.header-action{
+
+display:flex;
+
+gap:15px;
+
+}
+
+.glass-card{
+
+background:rgba(255,255,255,.06);
+
+backdrop-filter:blur(20px);
+
+border:1px solid rgba(255,255,255,.08);
+
+border-radius:22px;
+
+padding:25px;
+
+margin-bottom:30px;
+
+box-shadow:0 15px 35px rgba(0,0,0,.25);
+
+}
+
+.search-form{
+
+display:flex;
+
+gap:15px;
+
+align-items:center;
+
+}
+
+.search-form input{
+
+flex:1;
+
+padding:15px 18px;
+
+background:rgba(255,255,255,.05);
+
+border:1px solid rgba(255,255,255,.08);
+
+border-radius:14px;
+
+color:white;
+
+font-size:15px;
+
+outline:none;
+
+transition:.3s;
+
+}
+
+.search-form input:focus{
+
+border-color:#4F46E5;
+
+box-shadow:0 0 20px rgba(79,70,229,.25);
+
+}
+
+.search-form input::placeholder{
+
+color:#999;
+
+}
+
+/* TABLE */
+
+.modern-table{
+
+width:100%;
+
+border-collapse:collapse;
+
+}
+
+.modern-table thead{
+
+background:rgba(255,255,255,.06);
+
+}
+
+.modern-table th{
+
+padding:18px;
+
+font-weight:600;
+
+text-align:left;
+
+font-size:15px;
+
+}
+
+.modern-table td{
+
+padding:20px 18px;
+
+border-top:1px solid rgba(255,255,255,.06);
+
+vertical-align:middle;
+
+}
+
+.modern-table tbody tr{
+
+transition:.25s;
+
+}
+
+.modern-table tbody tr:hover{
+
+background:rgba(79,70,229,.12);
+
+}
+
+/* TITLE */
+
+.survey-title h3{
+
+margin:0;
+
+font-size:17px;
+
+}
+
+.survey-title small{
+
+opacity:.6;
+
+}
+
+/* QUESTION */
+
+.question-badge{
+
+padding:8px 15px;
+
+border-radius:999px;
+
+background:rgba(6,182,212,.15);
+
+color:#67E8F9;
+
+font-size:13px;
+
+font-weight:600;
+
+}
+
+/* STATUS */
+
+.status{
+
+display:inline-flex;
+
+align-items:center;
+
+padding:8px 14px;
+
+border-radius:999px;
+
+font-size:13px;
+
+font-weight:700;
+
+}
+
+.status.active{
+
+background:rgba(34,197,94,.15);
+
+color:#4ADE80;
+
+}
+
+.status.inactive{
+
+background:rgba(239,68,68,.15);
+
+color:#F87171;
+
+}
+
+/* ACTION */
+
+.action-group{
+
+display:flex;
+
+gap:10px;
+
+align-items:center;
+
+}
+
+.action{
+
+width:42px;
+
+height:42px;
+
+display:flex;
+
+justify-content:center;
+
+align-items:center;
+
+border-radius:12px;
+
+text-decoration:none;
+
+border:none;
+
+cursor:pointer;
+
+font-size:18px;
+
+transition:.3s;
+
+}
+
+.view{
+
+background:#2563EB;
+
+color:white;
+
+}
+
+.publish{
+
+background:#16A34A;
+
+color:white;
+
+}
+
+.edit{
+
+background:#D97706;
+
+color:white;
+
+}
+
+.delete{
+
+background:#DC2626;
+
+color:white;
+
+}
+
+.action:hover{
+
+transform:translateY(-4px) scale(1.08);
+
+}
+
+/* EMPTY */
+
+.empty{
+
+padding:60px;
+
+}
+
+.empty-box{
+
+text-align:center;
+
+}
+
+.empty-box h2{
+
+font-size:32px;
+
+margin-bottom:15px;
+
+}
+
+.empty-box p{
+
+opacity:.7;
+
+margin-bottom:20px;
+
+}
+
+/* PAGINATION */
+
+.pagination{
+
+display:flex;
+
+justify-content:center;
+
+align-items:center;
+
+gap:20px;
+
+margin:35px 0;
+
+}
+
+.pagination a{
+
+padding:12px 20px;
+
+background:#4F46E5;
+
+border-radius:12px;
+
+text-decoration:none;
+
+color:white;
+
+transition:.3s;
+
+}
+
+.pagination a:hover{
+
+transform:translateY(-3px);
+
+}
+
+.page-number{
+
+padding:12px 18px;
+
+background:rgba(255,255,255,.06);
+
+border-radius:12px;
+
+}
+
+/* RESPONSIVE */
+
+@media(max-width:1000px){
+
+.page-header{
+
+flex-direction:column;
+
+align-items:flex-start;
+
+}
+
+.header-action{
+
+width:100%;
+
+}
+
+.search-form{
+
+flex-direction:column;
+
+}
+
+.search-form input{
+
+width:100%;
+
+}
+
+.modern-table{
+
+display:block;
+
+overflow:auto;
+
+}
+
+}
+.form-group{
+margin-bottom:25px;
+}
+
+.form-group label{
+display:block;
+margin-bottom:10px;
+font-weight:600;
+}
+
+.form-group input,
+.form-group textarea,
+.form-group select{
+
+width:100%;
+padding:14px 18px;
+background:rgba(255,255,255,.05);
+border:1px solid rgba(255,255,255,.1);
+border-radius:14px;
+color:#fff;
+font-size:15px;
+outline:none;
+
+}
+
+.form-group textarea{
+resize:vertical;
+min-height:130px;
+}
+
+.form-group input:focus,
+.form-group textarea:focus,
+.form-group select:focus{
+
+border-color:#4F46E5;
+
+}
+
+.alert-danger{
+
+background:#dc2626;
+padding:15px 20px;
+border-radius:12px;
+margin-bottom:20px;
+font-weight:600;
+
+}
\ No newline at end of file
diff --git a/public/assets/table.css b/public/assets/table.css
new file mode 100644
index 00000000..5a827210
--- /dev/null
+++ b/public/assets/table.css
@@ -0,0 +1,200 @@
+body{
+ margin:0;
+ font-family:Inter,Arial,sans-serif;
+ color:white;
+}
+
+.page-wrapper{
+ display:flex;
+ min-height:100vh;
+}
+
+.page-content{
+ flex:1;
+ padding:40px;
+}
+
+.page-header{
+ display:flex;
+ justify-content:space-between;
+ align-items:center;
+ margin-bottom:30px;
+}
+
+.page-title{
+ font-size:36px;
+ font-weight:700;
+}
+
+.action-group{
+ display:flex;
+ gap:15px;
+}
+
+.search-box{
+ display:flex;
+ gap:10px;
+ margin-bottom:25px;
+}
+
+.search-box input{
+
+ flex:1;
+
+ padding:14px 18px;
+
+ border:none;
+
+ border-radius:14px;
+
+ background:rgba(255,255,255,.06);
+
+ color:white;
+
+}
+
+.search-box input::placeholder{
+
+color:#bbb;
+
+}
+
+.btn{
+
+padding:14px 22px;
+
+border:none;
+
+border-radius:14px;
+
+background:#4F46E5;
+
+color:white;
+
+cursor:pointer;
+
+text-decoration:none;
+
+transition:.3s;
+
+}
+
+.btn:hover{
+
+transform:translateY(-3px);
+
+}
+
+.btn-outline{
+
+padding:14px 22px;
+
+border-radius:14px;
+
+text-decoration:none;
+
+border:1px solid rgba(255,255,255,.12);
+
+color:white;
+
+}
+
+.table-card{
+
+background:rgba(255,255,255,.05);
+
+backdrop-filter:blur(25px);
+
+border-radius:20px;
+
+padding:25px;
+
+}
+
+table{
+
+width:100%;
+
+border-collapse:collapse;
+
+}
+
+thead{
+
+background:rgba(79,70,229,.2);
+
+}
+
+th{
+
+padding:18px;
+
+text-align:left;
+
+}
+
+td{
+
+padding:18px;
+
+border-bottom:1px solid rgba(255,255,255,.08);
+
+}
+
+tr:hover{
+
+background:rgba(255,255,255,.04);
+
+}
+
+.badge{
+
+padding:8px 14px;
+
+border-radius:999px;
+
+font-size:13px;
+
+font-weight:600;
+
+}
+
+.badge-success{
+
+background:#22c55e33;
+
+color:#22C55E;
+
+}
+
+.badge-danger{
+
+background:#ef444433;
+
+color:#ef4444;
+
+}
+
+.action{
+
+display:flex;
+
+gap:10px;
+
+flex-wrap:wrap;
+
+}
+
+.pagination{
+
+display:flex;
+
+justify-content:center;
+
+align-items:center;
+
+gap:15px;
+
+margin-top:30px;
+
+}
diff --git a/public/assets/videos/dashboard.mp4 b/public/assets/videos/dashboard.mp4
new file mode 100644
index 00000000..0de61902
Binary files /dev/null and b/public/assets/videos/dashboard.mp4 differ
diff --git a/public/assets/videos/login.mp4 b/public/assets/videos/login.mp4
new file mode 100644
index 00000000..e61a3e5c
Binary files /dev/null and b/public/assets/videos/login.mp4 differ
diff --git a/routes/api.js b/routes/api.js
new file mode 100644
index 00000000..73b0e72d
--- /dev/null
+++ b/routes/api.js
@@ -0,0 +1,13 @@
+const express = require("express");
+const router = express.Router();
+
+const surveyApiController = require("../controllers/api/surveyApiController");
+const questionApiController = require("../controllers/api/questionApiController");
+
+// API Survey
+router.get("/surveys", surveyApiController.index);
+
+// API Question
+router.get("/questions", questionApiController.index);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/assignment.js b/routes/assignment.js
new file mode 100644
index 00000000..5cd9b711
--- /dev/null
+++ b/routes/assignment.js
@@ -0,0 +1,17 @@
+const express = require("express");
+const router = express.Router();
+
+const assignmentController = require("../controllers/assignmentController");
+const { isAuthenticated } = require("../middlewares/auth");
+
+router.get("/", isAuthenticated, assignmentController.index);
+
+router.get("/create", isAuthenticated, assignmentController.createForm);
+router.post("/create", isAuthenticated, assignmentController.store);
+
+router.get("/edit/:id", isAuthenticated, assignmentController.editForm);
+router.post("/edit/:id", isAuthenticated, assignmentController.update);
+
+router.post("/delete/:id", isAuthenticated, assignmentController.destroy);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/option.js b/routes/option.js
new file mode 100644
index 00000000..791c55a5
--- /dev/null
+++ b/routes/option.js
@@ -0,0 +1,23 @@
+const express = require("express");
+const router = express.Router();
+
+const optionController = require("../controllers/optionController");
+
+// Semua option
+router.get("/", optionController.all);
+
+// Option berdasarkan question
+router.get("/question/:id", optionController.index);
+
+// Create
+router.get("/create/:questionId", optionController.createForm);
+router.post("/create/:questionId", optionController.store);
+
+// Edit
+router.get("/edit/:id", optionController.editForm);
+router.post("/edit/:id", optionController.update);
+
+// Delete
+router.post("/delete/:id", optionController.destroy);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/question.js b/routes/question.js
new file mode 100644
index 00000000..41b2038a
--- /dev/null
+++ b/routes/question.js
@@ -0,0 +1,52 @@
+const express = require("express");
+const router = express.Router();
+
+const questionController = require("../controllers/questionController");
+const { isAuthenticated } = require("../middlewares/auth");
+
+// Daftar semua pertanyaan
+router.get("/", isAuthenticated, questionController.index);
+
+// Daftar pertanyaan berdasarkan survey
+router.get(
+ "/survey/:id",
+ isAuthenticated,
+ questionController.bySurvey
+);
+
+// Form tambah pertanyaan untuk survey tertentu
+router.get(
+ "/create/:surveyId",
+ isAuthenticated,
+ questionController.createForm
+);
+
+// Simpan pertanyaan baru
+router.post(
+ "/create/:surveyId",
+ isAuthenticated,
+ questionController.store
+);
+
+// Form edit
+router.get(
+ "/edit/:id",
+ isAuthenticated,
+ questionController.editForm
+);
+
+// Update
+router.post(
+ "/edit/:id",
+ isAuthenticated,
+ questionController.update
+);
+
+// Hapus
+router.post(
+ "/delete/:id",
+ isAuthenticated,
+ questionController.destroy
+);
+
+module.exports = router;
\ No newline at end of file
diff --git a/routes/survey.js b/routes/survey.js
new file mode 100644
index 00000000..dffbedbd
--- /dev/null
+++ b/routes/survey.js
@@ -0,0 +1,31 @@
+const express = require("express");
+const router = express.Router();
+
+const surveyController = require("../controllers/surveyController");
+const { isAuthenticated } = require("../middlewares/auth");
+
+// List Survey
+router.get("/", isAuthenticated, surveyController.index);
+
+// Form Tambah Survey
+router.get("/create", isAuthenticated, surveyController.createForm);
+
+// Simpan Survey
+router.post("/create", isAuthenticated, surveyController.store);
+
+// Form Edit Survey
+router.get("/edit/:id", isAuthenticated, surveyController.editForm);
+
+// Update Survey
+router.post("/edit/:id", isAuthenticated, surveyController.update);
+
+// Publish Survey
+router.post("/publish/:id", isAuthenticated, surveyController.publish);
+
+// Export PDF
+router.get("/export/pdf", isAuthenticated, surveyController.exportPDF);
+
+// Hapus Survey
+router.post("/delete/:id", isAuthenticated, surveyController.destroy);
+
+module.exports = router;
\ No newline at end of file
diff --git a/test-results/.last-run.json b/test-results/.last-run.json
new file mode 100644
index 00000000..334ee4ff
--- /dev/null
+++ b/test-results/.last-run.json
@@ -0,0 +1,12 @@
+{
+ "status": "failed",
+ "failedTests": [
+ "426b5db5f2b87df21ca2-58224e800b8f84c619a4",
+ "ae4dfdd65416ab191397-8dfa68ccf409899d2dca",
+ "26ababbdc7ed30963a90-e97094cc63b8f5de4b76",
+ "828033178d578ddb03f7-a61690703be5a9b86b13",
+ "3e2e56fdebd4c66c6d42-c3b6b5499a7bbac23144",
+ "fe387cdde27477e11f6c-26038d3e3a1c86235446",
+ "e712d1b796dbefb72dca-3420e35eefd7bd6a5146"
+ ]
+}
\ No newline at end of file
diff --git a/test-results/assignment-create-Tambah-Assignment/error-context.md b/test-results/assignment-create-Tambah-Assignment/error-context.md
new file mode 100644
index 00000000..6c4a30bb
--- /dev/null
+++ b/test-results/assignment-create-Tambah-Assignment/error-context.md
@@ -0,0 +1,142 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: assignment-create.spec.js >> Tambah Assignment
+- Location: testing\assignment-create.spec.js:3:1
+
+# Error details
+
+```
+Test timeout of 30000ms exceeded.
+```
+
+```
+Error: page.selectOption: Test timeout of 30000ms exceeded.
+Call log:
+ - waiting for locator('select[name="survey_id"]')
+ - locator resolved to
+ - attempting select option action
+ 2 × waiting for element to be visible and enabled
+ - did not find some options
+ - retrying select option action
+ - waiting 20ms
+ 2 × waiting for element to be visible and enabled
+ - did not find some options
+ - retrying select option action
+ - waiting 100ms
+ 54 × waiting for element to be visible and enabled
+ - did not find some options
+ - retrying select option action
+ - waiting 500ms
+
+```
+
+# Page snapshot
+
+```yaml
+- generic [ref=e5]:
+ - generic [ref=e6]:
+ - generic [ref=e7]:
+ - img "Facultyware Logo" [ref=e8]
+ - generic [ref=e9]:
+ - heading "Facultyware" [level=2] [ref=e10]
+ - text: Survey Management System
+ - generic [ref=e11]:
+ - link "🏠 Dashboard" [ref=e12] [cursor=pointer]:
+ - /url: /home
+ - link "📋 Survey" [ref=e13] [cursor=pointer]:
+ - /url: /survey
+ - link "❓ Question" [ref=e14] [cursor=pointer]:
+ - /url: /question
+ - link "🔗 Assignment" [ref=e15] [cursor=pointer]:
+ - /url: /assignment
+ - generic [ref=e16]:
+ - generic [ref=e17]: Facultyware v2.0
+ - link "🚪 Logout" [ref=e18] [cursor=pointer]:
+ - /url: /logout
+ - generic [ref=e19]:
+ - generic [ref=e20]:
+ - generic [ref=e21]:
+ - generic [ref=e22]: Welcome Back, hanif 👋
+ - paragraph [ref=e23]: Faculty Survey Management System
+ - generic [ref=e24]:
+ - generic [ref=e28]: System Online
+ - generic [ref=e29]:
+ - generic [ref=e30]: H
+ - generic [ref=e31]:
+ - strong [ref=e32]: hanif
+ - text: Administrator
+ - generic [ref=e33]:
+ - generic [ref=e34]:
+ - text: Survey Assignment
+ - heading "Create Assignment" [level=1] [ref=e35]
+ - paragraph [ref=e36]: Assign a question into a survey.
+ - link "← Back" [ref=e37] [cursor=pointer]:
+ - /url: /assignment
+ - generic [ref=e39]:
+ - generic [ref=e40]:
+ - generic [ref=e41]: Survey
+ - combobox [ref=e42]:
+ - option "trhwth" [selected]
+ - generic [ref=e43]:
+ - generic [ref=e44]: Question
+ - combobox [ref=e45]:
+ - option "Bagaimana fasilitas ruang kelas?" [selected]
+ - option "Bagaimana keamanan lingkungan kampus?"
+ - option "Bagaimana kebersihan lingkungan kampus?"
+ - option "Bagaimana kedisiplinan dosen dalam mengajar?"
+ - option "Bagaimana kualitas jaringan internet kampus?"
+ - option "Bagaimana kualitas materi perkuliahan?"
+ - option "Bagaimana kualitas pelayanan dosen?"
+ - option "Bagaimana pelayanan administrasi akademik?"
+ - option "Bagaimana pelayanan perpustakaan?"
+ - option "Seberapa puas Anda terhadap layanan kampus secara keseluruhan?"
+ - generic [ref=e46]:
+ - generic [ref=e47]: Order
+ - spinbutton [ref=e48]: "1"
+ - button "💾 Save Assignment" [ref=e49]
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Tambah Assignment', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]','hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]','hanif123');
+ 9 |
+ 10 | await page.getByRole('button').click();
+ 11 |
+ 12 | await page.goto('http://localhost:3000/assignment/create');
+ 13 |
+> 14 | await page.selectOption(
+ | ^ Error: page.selectOption: Test timeout of 30000ms exceeded.
+ 15 | 'select[name="survey_id"]',
+ 16 | { index: 1 }
+ 17 | );
+ 18 |
+ 19 | await page.selectOption(
+ 20 | 'select[name="survey_question_id"]',
+ 21 | { index: 1 }
+ 22 | );
+ 23 |
+ 24 | await page.fill(
+ 25 | 'input[name="order"]',
+ 26 | '99'
+ 27 | );
+ 28 |
+ 29 | await page.locator('button.hero-btn').click();
+ 30 |
+ 31 | await expect(page).toHaveURL(/assignment/);
+ 32 |
+ 33 | });
+```
\ No newline at end of file
diff --git a/test-results/assignment-delete-Tombol-Hapus-Assignment-tampil/error-context.md b/test-results/assignment-delete-Tombol-Hapus-Assignment-tampil/error-context.md
new file mode 100644
index 00000000..63176d61
--- /dev/null
+++ b/test-results/assignment-delete-Tombol-Hapus-Assignment-tampil/error-context.md
@@ -0,0 +1,106 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: assignment-delete.spec.js >> Tombol Hapus Assignment tampil
+- Location: testing\assignment-delete.spec.js:3:1
+
+# Error details
+
+```
+Error: expect(locator).toBeVisible() failed
+
+Locator: locator('.action.delete').first()
+Expected: visible
+Timeout: 5000ms
+Error: element(s) not found
+
+Call log:
+ - Expect "toBeVisible" with timeout 5000ms
+ - waiting for locator('.action.delete').first()
+
+```
+
+```yaml
+- img "Facultyware Logo"
+- heading "Facultyware" [level=2]
+- text: Survey Management System
+- link "🏠 Dashboard":
+ - /url: /home
+- link "📋 Survey":
+ - /url: /survey
+- link "❓ Question":
+ - /url: /question
+- link "🔗 Assignment":
+ - /url: /assignment
+- text: Facultyware v2.0
+- link "🚪 Logout":
+ - /url: /logout
+- text: Welcome Back, hanif 👋
+- paragraph: Faculty Survey Management System
+- text: System Online H
+- strong: hanif
+- text: Administrator Survey Assignment
+- heading "Assignment Management" [level=1]
+- paragraph: Connect surveys with their questions.
+- link "➕ New Assignment":
+ - /url: /assignment/create
+- text: 🔗
+- paragraph: Total Assignment
+- heading "0" [level=2]
+- text: 📄
+- paragraph: Current Page
+- heading "1" [level=2]
+- text: 📚
+- paragraph: Total Page
+- heading "0" [level=2]
+- text: 🔍
+- paragraph: Search
+- heading "-" [level=2]
+- textbox "Search survey or question..."
+- button "Search"
+- link "Reset":
+ - /url: /assignment
+- table:
+ - rowgroup:
+ - row "No Survey Question Order Action":
+ - columnheader "No"
+ - columnheader "Survey"
+ - columnheader "Question"
+ - columnheader "Order"
+ - columnheader "Action"
+ - rowgroup:
+ - row "📭 No Assignment No assignment available.":
+ - cell "📭 No Assignment No assignment available.":
+ - heading "📭 No Assignment" [level=2]
+ - paragraph: No assignment available.
+- text: Page 1 of 0
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Tombol Hapus Assignment tampil', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/assignment');
+ 13 |
+ 14 | await expect(
+ 15 | page.locator(".action.delete").first()
+> 16 | ).toBeVisible();
+ | ^ Error: expect(locator).toBeVisible() failed
+ 17 |
+ 18 | });
+```
\ No newline at end of file
diff --git a/test-results/assignment-update-Halaman-Edit-Assignment/error-context.md b/test-results/assignment-update-Halaman-Edit-Assignment/error-context.md
new file mode 100644
index 00000000..1bef5abe
--- /dev/null
+++ b/test-results/assignment-update-Halaman-Edit-Assignment/error-context.md
@@ -0,0 +1,132 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: assignment-update.spec.js >> Halaman Edit Assignment
+- Location: testing\assignment-update.spec.js:3:1
+
+# Error details
+
+```
+Test timeout of 30000ms exceeded.
+```
+
+```
+Error: locator.click: Test timeout of 30000ms exceeded.
+Call log:
+ - waiting for locator('.action.edit').first()
+
+```
+
+# Page snapshot
+
+```yaml
+- generic [ref=e5]:
+ - generic [ref=e6]:
+ - generic [ref=e7]:
+ - img "Facultyware Logo" [ref=e8]
+ - generic [ref=e9]:
+ - heading "Facultyware" [level=2] [ref=e10]
+ - text: Survey Management System
+ - generic [ref=e11]:
+ - link "🏠 Dashboard" [ref=e12] [cursor=pointer]:
+ - /url: /home
+ - link "📋 Survey" [ref=e13] [cursor=pointer]:
+ - /url: /survey
+ - link "❓ Question" [ref=e14] [cursor=pointer]:
+ - /url: /question
+ - link "🔗 Assignment" [ref=e15] [cursor=pointer]:
+ - /url: /assignment
+ - generic [ref=e16]:
+ - generic [ref=e17]: Facultyware v2.0
+ - link "🚪 Logout" [ref=e18] [cursor=pointer]:
+ - /url: /logout
+ - generic [ref=e19]:
+ - generic [ref=e20]:
+ - generic [ref=e21]:
+ - generic [ref=e22]: Welcome Back, hanif 👋
+ - paragraph [ref=e23]: Faculty Survey Management System
+ - generic [ref=e24]:
+ - generic [ref=e28]: System Online
+ - generic [ref=e29]:
+ - generic [ref=e30]: H
+ - generic [ref=e31]:
+ - strong [ref=e32]: hanif
+ - text: Administrator
+ - generic [ref=e33]:
+ - generic [ref=e34]:
+ - text: Survey Assignment
+ - heading "Assignment Management" [level=1] [ref=e35]
+ - paragraph [ref=e36]: Connect surveys with their questions.
+ - link "➕ New Assignment" [ref=e38] [cursor=pointer]:
+ - /url: /assignment/create
+ - generic [ref=e39]:
+ - generic [ref=e40] [cursor=pointer]:
+ - generic [ref=e41]: 🔗
+ - generic [ref=e42]:
+ - paragraph [ref=e43]: Total Assignment
+ - heading "0" [level=2] [ref=e44]
+ - generic [ref=e45] [cursor=pointer]:
+ - generic [ref=e46]: 📄
+ - generic [ref=e47]:
+ - paragraph [ref=e48]: Current Page
+ - heading "1" [level=2] [ref=e49]
+ - generic [ref=e50] [cursor=pointer]:
+ - generic [ref=e51]: 📚
+ - generic [ref=e52]:
+ - paragraph [ref=e53]: Total Page
+ - heading "0" [level=2] [ref=e54]
+ - generic [ref=e55] [cursor=pointer]:
+ - generic [ref=e56]: 🔍
+ - generic [ref=e57]:
+ - paragraph [ref=e58]: Search
+ - heading "-" [level=2] [ref=e59]
+ - generic [ref=e61]:
+ - textbox "Search survey or question..." [ref=e62]
+ - button "Search" [ref=e63]
+ - link "Reset" [ref=e64] [cursor=pointer]:
+ - /url: /assignment
+ - table [ref=e66]:
+ - rowgroup [ref=e67]:
+ - row "No Survey Question Order Action" [ref=e68]:
+ - columnheader "No" [ref=e69]
+ - columnheader "Survey" [ref=e70]
+ - columnheader "Question" [ref=e71]
+ - columnheader "Order" [ref=e72]
+ - columnheader "Action" [ref=e73]
+ - rowgroup [ref=e74]:
+ - row "📭 No Assignment No assignment available." [ref=e75]:
+ - cell "📭 No Assignment No assignment available." [ref=e76]:
+ - generic [ref=e77]:
+ - heading "📭 No Assignment" [level=2] [ref=e78]
+ - paragraph [ref=e79]: No assignment available.
+ - generic [ref=e81]: Page 1 of 0
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Halaman Edit Assignment', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/assignment');
+ 13 |
+> 14 | await page.locator(".action.edit").first().click();
+ | ^ Error: locator.click: Test timeout of 30000ms exceeded.
+ 15 |
+ 16 | await expect(page.locator('body')).toContainText('Edit Assignment');
+ 17 |
+ 18 | });
+```
\ No newline at end of file
diff --git a/test-results/question-Membuka-halaman-Pertanyaan/error-context.md b/test-results/question-Membuka-halaman-Pertanyaan/error-context.md
new file mode 100644
index 00000000..06b36f61
--- /dev/null
+++ b/test-results/question-Membuka-halaman-Pertanyaan/error-context.md
@@ -0,0 +1,61 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: question.spec.js >> Membuka halaman Pertanyaan
+- Location: testing\question.spec.js:3:1
+
+# Error details
+
+```
+Error: expect(locator).toContainText(expected) failed
+
+Locator: locator('h1')
+Timeout: 5000ms
+- Expected substring - 1
++ Received string + 3
+
+- Question
++
++ Data Survey
++
+
+Call log:
+ - Expect "toContainText" with timeout 5000ms
+ - waiting for locator('h1')
+ 13 × locator resolved to ↵ Data Survey↵
+ - unexpected value "
+ Data Survey
+ "
+
+```
+
+```yaml
+- heading "Data Survey" [level=1]
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Membuka halaman Pertanyaan', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/question/survey/1');
+ 13 |
+> 14 | await expect(page.locator('h1')).toContainText('Question');
+ | ^ Error: expect(locator).toContainText(expected) failed
+ 15 |
+ 16 | });
+```
\ No newline at end of file
diff --git a/test-results/question-create-Tambah-Pertanyaan/error-context.md b/test-results/question-create-Tambah-Pertanyaan/error-context.md
new file mode 100644
index 00000000..5efee2a1
--- /dev/null
+++ b/test-results/question-create-Tambah-Pertanyaan/error-context.md
@@ -0,0 +1,59 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: question-create.spec.js >> Tambah Pertanyaan
+- Location: testing\question-create.spec.js:3:1
+
+# Error details
+
+```
+Error: expect(page).toHaveURL(expected) failed
+
+Expected pattern: /question\/survey\/1/
+Received string: "http://localhost:3000/question/create/1"
+Timeout: 5000ms
+
+Call log:
+ - Expect "toHaveURL" with timeout 5000ms
+ 13 × unexpected value "http://localhost:3000/question/create/1"
+
+```
+
+```yaml
+- 'heading "Cannot add or update a child row: a foreign key constraint fails (`facultyware`.`survey_question_assignments`, CONSTRAINT `survey_question_assignments_survey_id_foreign` FOREIGN KEY (`survey_id`) REFERENCES `surveys` (`id`))" [level=1]'
+- heading [level=2]
+- text: "Error: Cannot add or update a child row: a foreign key constraint fails (`facultyware`.`survey_question_assignments`, CONSTRAINT `survey_question_assignments_survey_id_foreign` FOREIGN KEY (`survey_id`) REFERENCES `surveys` (`id`)) at store (D:\\information system\\PWEB\\a12 pweb backup\\facultyware\\controllers\\questionController.js:161:18) at process.processTicksAndRejections (node:internal/process/task_queues:104:5)"
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Tambah Pertanyaan', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/question/create/1');
+ 13 |
+ 14 | await page.fill('textarea[name="question_text"]', 'Pertanyaan Playwright');
+ 15 |
+ 16 | await page.selectOption('select[name="type"]', 'text');
+ 17 |
+ 18 | await page.click('button[type="submit"]');
+ 19 |
+> 20 | await expect(page).toHaveURL(/question\/survey\/1/);
+ | ^ Error: expect(page).toHaveURL(expected) failed
+ 21 |
+ 22 | });
+```
\ No newline at end of file
diff --git a/test-results/question-delete-Tombol-Hapus-Pertanyaan-tampil/error-context.md b/test-results/question-delete-Tombol-Hapus-Pertanyaan-tampil/error-context.md
new file mode 100644
index 00000000..f9047f30
--- /dev/null
+++ b/test-results/question-delete-Tombol-Hapus-Pertanyaan-tampil/error-context.md
@@ -0,0 +1,141 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: question-delete.spec.js >> Tombol Hapus Pertanyaan tampil
+- Location: testing\question-delete.spec.js:3:1
+
+# Error details
+
+```
+Error: expect(locator).toBeVisible() failed
+
+Locator: locator('button[title="Delete Question"]').first()
+Expected: visible
+Timeout: 5000ms
+Error: element(s) not found
+
+Call log:
+ - Expect "toBeVisible" with timeout 5000ms
+ - waiting for locator('button[title="Delete Question"]').first()
+
+```
+
+```yaml
+- img "Facultyware Logo"
+- heading "Facultyware" [level=2]
+- text: Survey Management System
+- link "🏠 Dashboard":
+ - /url: /home
+- link "📋 Survey":
+ - /url: /survey
+- link "❓ Question":
+ - /url: /question
+- link "🔗 Assignment":
+ - /url: /assignment
+- text: Facultyware v2.0
+- link "🚪 Logout":
+ - /url: /logout
+- text: Welcome Back, hanif 👋
+- paragraph: Faculty Survey Management System
+- text: System Online H
+- strong: hanif
+- text: Administrator Survey Management
+- heading "Data Survey" [level=1]
+- paragraph: Create, manage and publish faculty surveys professionally.
+- link "📄 Export PDF":
+ - /url: /survey/export/pdf
+- link "➕ New Survey":
+ - /url: /survey/create
+- text: 📋
+- paragraph: Total Survey
+- heading "2" [level=2]
+- text: 📄
+- paragraph: Current Page
+- heading "1" [level=2]
+- text: 📚
+- paragraph: Total Page
+- heading "1" [level=2]
+- text: 🔍
+- paragraph: Keyword
+- heading "-" [level=2]
+- textbox "Search survey..."
+- button "Search"
+- link "Reset":
+ - /url: /survey
+- table:
+ - rowgroup:
+ - row "No Survey Question Start End Status Action":
+ - columnheader "No"
+ - columnheader "Survey"
+ - columnheader "Question"
+ - columnheader "Start"
+ - columnheader "End"
+ - columnheader "Status"
+ - columnheader "Action"
+ - rowgroup:
+ - row "1 Survey Playwright Faculty Survey 0 Question 23/6/2026 30/6/2026 🔴 Inactive 📋 🚀 ✏️ 🗑️":
+ - cell "1"
+ - cell "Survey Playwright Faculty Survey":
+ - heading "Survey Playwright" [level=3]
+ - text: Faculty Survey
+ - cell "0 Question"
+ - cell "23/6/2026"
+ - cell "30/6/2026"
+ - cell "🔴 Inactive"
+ - cell "📋 🚀 ✏️ 🗑️":
+ - link "📋":
+ - /url: /question/survey/25
+ - button "🚀"
+ - link "✏️":
+ - /url: /survey/edit/25
+ - button "🗑️"
+ - row "2 trhwth Faculty Survey 0 Question 25/6/2026 26/6/2026 🔴 Inactive 📋 🚀 ✏️ 🗑️":
+ - cell "2"
+ - cell "trhwth Faculty Survey":
+ - heading "trhwth" [level=3]
+ - text: Faculty Survey
+ - cell "0 Question"
+ - cell "25/6/2026"
+ - cell "26/6/2026"
+ - cell "🔴 Inactive"
+ - cell "📋 🚀 ✏️ 🗑️":
+ - link "📋":
+ - /url: /question/survey/24
+ - button "🚀"
+ - link "✏️":
+ - /url: /survey/edit/24
+ - button "🗑️"
+- text: Page
+- strong: "1"
+- text: of
+- strong: "1"
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Tombol Hapus Pertanyaan tampil', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/question/survey/1');
+ 13 |
+ 14 | await expect(
+ 15 | page.locator('button[title="Delete Question"]').first()
+> 16 | ).toBeVisible();
+ | ^ Error: expect(locator).toBeVisible() failed
+ 17 |
+ 18 | });
+```
\ No newline at end of file
diff --git a/test-results/question-update-Halaman-Edit-Pertanyaan/error-context.md b/test-results/question-update-Halaman-Edit-Pertanyaan/error-context.md
new file mode 100644
index 00000000..67add298
--- /dev/null
+++ b/test-results/question-update-Halaman-Edit-Pertanyaan/error-context.md
@@ -0,0 +1,174 @@
+# Instructions
+
+- Following Playwright test failed.
+- Explain why, be concise, respect Playwright best practices.
+- Provide a snippet of code with the fix, if possible.
+
+# Test info
+
+- Name: question-update.spec.js >> Halaman Edit Pertanyaan
+- Location: testing\question-update.spec.js:3:1
+
+# Error details
+
+```
+Test timeout of 30000ms exceeded.
+```
+
+```
+Error: locator.click: Test timeout of 30000ms exceeded.
+Call log:
+ - waiting for locator('a[title="Edit Question"]').first()
+
+```
+
+# Page snapshot
+
+```yaml
+- generic [ref=e5]:
+ - generic [ref=e6]:
+ - generic [ref=e7]:
+ - img "Facultyware Logo" [ref=e8]
+ - generic [ref=e9]:
+ - heading "Facultyware" [level=2] [ref=e10]
+ - text: Survey Management System
+ - generic [ref=e11]:
+ - link "🏠 Dashboard" [ref=e12] [cursor=pointer]:
+ - /url: /home
+ - link "📋 Survey" [ref=e13] [cursor=pointer]:
+ - /url: /survey
+ - link "❓ Question" [ref=e14] [cursor=pointer]:
+ - /url: /question
+ - link "🔗 Assignment" [ref=e15] [cursor=pointer]:
+ - /url: /assignment
+ - generic [ref=e16]:
+ - generic [ref=e17]: Facultyware v2.0
+ - link "🚪 Logout" [ref=e18] [cursor=pointer]:
+ - /url: /logout
+ - generic [ref=e19]:
+ - generic [ref=e20]:
+ - generic [ref=e21]:
+ - generic [ref=e22]: Welcome Back, hanif 👋
+ - paragraph [ref=e23]: Faculty Survey Management System
+ - generic [ref=e24]:
+ - generic [ref=e28]: System Online
+ - generic [ref=e29]:
+ - generic [ref=e30]: H
+ - generic [ref=e31]:
+ - strong [ref=e32]: hanif
+ - text: Administrator
+ - generic [ref=e33]:
+ - generic [ref=e34]:
+ - text: Survey Management
+ - heading "Data Survey" [level=1] [ref=e35]
+ - paragraph [ref=e36]: Create, manage and publish faculty surveys professionally.
+ - generic [ref=e37]:
+ - link "📄 Export PDF" [ref=e38] [cursor=pointer]:
+ - /url: /survey/export/pdf
+ - link "➕ New Survey" [ref=e39] [cursor=pointer]:
+ - /url: /survey/create
+ - generic [ref=e40]:
+ - generic [ref=e41] [cursor=pointer]:
+ - generic [ref=e42]: 📋
+ - generic [ref=e43]:
+ - paragraph [ref=e44]: Total Survey
+ - heading "2" [level=2] [ref=e45]
+ - generic [ref=e46] [cursor=pointer]:
+ - generic [ref=e47]: 📄
+ - generic [ref=e48]:
+ - paragraph [ref=e49]: Current Page
+ - heading "1" [level=2] [ref=e50]
+ - generic [ref=e51] [cursor=pointer]:
+ - generic [ref=e52]: 📚
+ - generic [ref=e53]:
+ - paragraph [ref=e54]: Total Page
+ - heading "1" [level=2] [ref=e55]
+ - generic [ref=e56] [cursor=pointer]:
+ - generic [ref=e57]: 🔍
+ - generic [ref=e58]:
+ - paragraph [ref=e59]: Keyword
+ - heading "-" [level=2] [ref=e60]
+ - generic [ref=e62]:
+ - textbox "Search survey..." [ref=e63]
+ - button "Search" [ref=e64]
+ - link "Reset" [ref=e65] [cursor=pointer]:
+ - /url: /survey
+ - table [ref=e67]:
+ - rowgroup [ref=e68]:
+ - row "No Survey Question Start End Status Action" [ref=e69]:
+ - columnheader "No" [ref=e70]
+ - columnheader "Survey" [ref=e71]
+ - columnheader "Question" [ref=e72]
+ - columnheader "Start" [ref=e73]
+ - columnheader "End" [ref=e74]
+ - columnheader "Status" [ref=e75]
+ - columnheader "Action" [ref=e76]
+ - rowgroup [ref=e77]:
+ - row "1 Survey Playwright Faculty Survey 0 Question 23/6/2026 30/6/2026 🔴 Inactive 📋 🚀 ✏️ 🗑️" [ref=e78]:
+ - cell "1" [ref=e79]
+ - cell "Survey Playwright Faculty Survey" [ref=e80]:
+ - generic [ref=e81]:
+ - heading "Survey Playwright" [level=3] [ref=e82]
+ - text: Faculty Survey
+ - cell "0 Question" [ref=e83]
+ - cell "23/6/2026" [ref=e84]
+ - cell "30/6/2026" [ref=e85]
+ - cell "🔴 Inactive" [ref=e86]:
+ - generic [ref=e87]: 🔴 Inactive
+ - cell "📋 🚀 ✏️ 🗑️" [ref=e88]:
+ - generic [ref=e89]:
+ - link "📋" [ref=e90] [cursor=pointer]:
+ - /url: /question/survey/25
+ - button "🚀" [ref=e92] [cursor=pointer]
+ - link "✏️" [ref=e93] [cursor=pointer]:
+ - /url: /survey/edit/25
+ - button "🗑️" [ref=e95] [cursor=pointer]
+ - row "2 trhwth Faculty Survey 0 Question 25/6/2026 26/6/2026 🔴 Inactive 📋 🚀 ✏️ 🗑️" [ref=e96]:
+ - cell "2" [ref=e97]
+ - cell "trhwth Faculty Survey" [ref=e98]:
+ - generic [ref=e99]:
+ - heading "trhwth" [level=3] [ref=e100]
+ - text: Faculty Survey
+ - cell "0 Question" [ref=e101]
+ - cell "25/6/2026" [ref=e102]
+ - cell "26/6/2026" [ref=e103]
+ - cell "🔴 Inactive" [ref=e104]:
+ - generic [ref=e105]: 🔴 Inactive
+ - cell "📋 🚀 ✏️ 🗑️" [ref=e106]:
+ - generic [ref=e107]:
+ - link "📋" [ref=e108] [cursor=pointer]:
+ - /url: /question/survey/24
+ - button "🚀" [ref=e110] [cursor=pointer]
+ - link "✏️" [ref=e111] [cursor=pointer]:
+ - /url: /survey/edit/24
+ - button "🗑️" [ref=e113] [cursor=pointer]
+ - generic [ref=e115]:
+ - text: Page
+ - strong [ref=e116]: "1"
+ - text: of
+ - strong [ref=e117]: "1"
+```
+
+# Test source
+
+```ts
+ 1 | const { test, expect } = require('@playwright/test');
+ 2 |
+ 3 | test('Halaman Edit Pertanyaan', async ({ page }) => {
+ 4 |
+ 5 | await page.goto('http://localhost:3000/login');
+ 6 |
+ 7 | await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ 8 | await page.fill('input[name="password"]', 'hanif123');
+ 9 |
+ 10 | await page.click('button[type="submit"]');
+ 11 |
+ 12 | await page.goto('http://localhost:3000/question/survey/1');
+ 13 |
+> 14 | await page.locator('a[title="Edit Question"]').first().click();
+ | ^ Error: locator.click: Test timeout of 30000ms exceeded.
+ 15 |
+ 16 | await expect(page.locator('h1')).toContainText('Edit');
+ 17 |
+ 18 | });
+```
\ No newline at end of file
diff --git a/testing/assignment-create.spec.js b/testing/assignment-create.spec.js
new file mode 100644
index 00000000..56f5f615
--- /dev/null
+++ b/testing/assignment-create.spec.js
@@ -0,0 +1,33 @@
+const { test, expect } = require('@playwright/test');
+
+test('Tambah Assignment', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]','hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]','hanif123');
+
+ await page.getByRole('button').click();
+
+ await page.goto('http://localhost:3000/assignment/create');
+
+ await page.selectOption(
+ 'select[name="survey_id"]',
+ { index: 1 }
+ );
+
+ await page.selectOption(
+ 'select[name="survey_question_id"]',
+ { index: 1 }
+ );
+
+ await page.fill(
+ 'input[name="order"]',
+ '99'
+ );
+
+ await page.locator('button.hero-btn').click();
+
+ await expect(page).toHaveURL(/assignment/);
+
+});
\ No newline at end of file
diff --git a/testing/assignment-delete.spec.js b/testing/assignment-delete.spec.js
new file mode 100644
index 00000000..8f4b58e5
--- /dev/null
+++ b/testing/assignment-delete.spec.js
@@ -0,0 +1,18 @@
+const { test, expect } = require('@playwright/test');
+
+test('Tombol Hapus Assignment tampil', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/assignment');
+
+ await expect(
+ page.locator(".action.delete").first()
+ ).toBeVisible();
+
+});
\ No newline at end of file
diff --git a/testing/assignment-list.spec.js b/testing/assignment-list.spec.js
new file mode 100644
index 00000000..3e48f0cf
--- /dev/null
+++ b/testing/assignment-list.spec.js
@@ -0,0 +1,16 @@
+const { test, expect } = require('@playwright/test');
+
+test('Menampilkan Data Assignment', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.locator('button[type="submit"]').click();
+
+ await page.goto('http://localhost:3000/assignment');
+
+ await expect(page.locator('h1')).toContainText('Assignment Management');
+
+});
\ No newline at end of file
diff --git a/testing/assignment-update.spec.js b/testing/assignment-update.spec.js
new file mode 100644
index 00000000..ace9c2a6
--- /dev/null
+++ b/testing/assignment-update.spec.js
@@ -0,0 +1,18 @@
+const { test, expect } = require('@playwright/test');
+
+test('Halaman Edit Assignment', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/assignment');
+
+ await page.locator(".action.edit").first().click();
+
+ await expect(page.locator('body')).toContainText('Edit Assignment');
+
+});
\ No newline at end of file
diff --git a/testing/create-survey.spec.js b/testing/create-survey.spec.js
new file mode 100644
index 00000000..c73ecb15
--- /dev/null
+++ b/testing/create-survey.spec.js
@@ -0,0 +1,24 @@
+const { test, expect } = require('@playwright/test');
+
+test('Tambah Survey', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.getByRole('button').click();
+
+ await page.goto('http://localhost:3000/survey/create');
+
+ await page.fill('input[name="title"]', 'Survey Playwright');
+ await page.fill('textarea[name="description"]', 'Survey dari Playwright');
+
+ await page.fill('input[name="start_date"]', '2026-06-23');
+ await page.fill('input[name="end_date"]', '2026-06-30');
+
+ await page.locator('button.hero-btn').click();
+
+ await expect(page).toHaveURL(/survey/);
+
+});
\ No newline at end of file
diff --git a/testing/home.spec.js b/testing/home.spec.js
new file mode 100644
index 00000000..e69de29b
diff --git a/testing/login.spec.js b/testing/login.spec.js
new file mode 100644
index 00000000..fff39861
--- /dev/null
+++ b/testing/login.spec.js
@@ -0,0 +1,15 @@
+const { test, expect } = require('@playwright/test');
+
+test('Login Admin', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await expect(page).toHaveURL(/home/);
+
+});
\ No newline at end of file
diff --git a/testing/logout.spec.js b/testing/logout.spec.js
new file mode 100644
index 00000000..f6abc0d9
--- /dev/null
+++ b/testing/logout.spec.js
@@ -0,0 +1,16 @@
+const { test, expect } = require('@playwright/test');
+
+test('Logout', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]','hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]','hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/logout');
+
+ await expect(page).toHaveURL(/login/);
+
+});
diff --git a/testing/option-create.spec.js b/testing/option-create.spec.js
new file mode 100644
index 00000000..b182e902
--- /dev/null
+++ b/testing/option-create.spec.js
@@ -0,0 +1,22 @@
+const { test, expect } = require('@playwright/test');
+
+test('Tambah Opsi Jawaban', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/option/create/1');
+
+ await page.fill('input[name="option_text"]', 'Opsi Playwright');
+
+ await page.fill('input[name="weight"]', '5');
+
+ await page.click('button[type="submit"]');
+
+ await expect(page).toHaveURL(/option\/question\/1/);
+
+});
\ No newline at end of file
diff --git a/testing/option-delete.spec.js b/testing/option-delete.spec.js
new file mode 100644
index 00000000..3f5e7ebd
--- /dev/null
+++ b/testing/option-delete.spec.js
@@ -0,0 +1,18 @@
+const { test, expect } = require('@playwright/test');
+
+test('Tombol Hapus Opsi tampil', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/option/question/1');
+
+ await expect(
+ page.locator('button[title="Delete Option"]').first()
+ ).toBeVisible();
+
+});
\ No newline at end of file
diff --git a/testing/option-list.spec.js b/testing/option-list.spec.js
new file mode 100644
index 00000000..3ff2b570
--- /dev/null
+++ b/testing/option-list.spec.js
@@ -0,0 +1,16 @@
+const { test, expect } = require('@playwright/test');
+
+test('Menampilkan Data Opsi Jawaban', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.locator('button[type="submit"]').click();
+
+ await page.goto('http://localhost:3000/option/question/1');
+
+ await expect(page.locator('body')).toContainText('Option');
+
+});
\ No newline at end of file
diff --git a/testing/option-update.spec.js b/testing/option-update.spec.js
new file mode 100644
index 00000000..bc2901fb
--- /dev/null
+++ b/testing/option-update.spec.js
@@ -0,0 +1,18 @@
+const { test, expect } = require('@playwright/test');
+
+test('Halaman Edit Opsi', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/option/question/1');
+
+ await page.locator('a[title="Edit Option"]').first().click();
+
+ await expect(page.locator('h1')).toContainText('Edit');
+
+});
\ No newline at end of file
diff --git a/testing/question-api.spec.js b/testing/question-api.spec.js
new file mode 100644
index 00000000..f34d67e9
--- /dev/null
+++ b/testing/question-api.spec.js
@@ -0,0 +1,15 @@
+const { test, expect } = require('@playwright/test');
+
+test('REST API Question', async ({ request }) => {
+
+ const response = await request.get(
+ 'http://localhost:3000/api/questions'
+ );
+
+ expect(response.ok()).toBeTruthy();
+
+ const data = await response.json();
+
+ expect(Array.isArray(data)).toBeTruthy();
+
+});
\ No newline at end of file
diff --git a/testing/question-create.spec.js b/testing/question-create.spec.js
new file mode 100644
index 00000000..5c5c09d0
--- /dev/null
+++ b/testing/question-create.spec.js
@@ -0,0 +1,22 @@
+const { test, expect } = require('@playwright/test');
+
+test('Tambah Pertanyaan', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/question/create/1');
+
+ await page.fill('textarea[name="question_text"]', 'Pertanyaan Playwright');
+
+ await page.selectOption('select[name="type"]', 'text');
+
+ await page.click('button[type="submit"]');
+
+ await expect(page).toHaveURL(/question\/survey\/1/);
+
+});
\ No newline at end of file
diff --git a/testing/question-delete.spec.js b/testing/question-delete.spec.js
new file mode 100644
index 00000000..b46a0c06
--- /dev/null
+++ b/testing/question-delete.spec.js
@@ -0,0 +1,18 @@
+const { test, expect } = require('@playwright/test');
+
+test('Tombol Hapus Pertanyaan tampil', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/question/survey/1');
+
+ await expect(
+ page.locator('button[title="Delete Question"]').first()
+ ).toBeVisible();
+
+});
\ No newline at end of file
diff --git a/testing/question-list.spec.js b/testing/question-list.spec.js
new file mode 100644
index 00000000..a30b10c1
--- /dev/null
+++ b/testing/question-list.spec.js
@@ -0,0 +1,16 @@
+const { test, expect } = require('@playwright/test');
+
+test('Menampilkan Data Pertanyaan', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.locator('button[type="submit"]').click();
+
+ await page.goto('http://localhost:3000/question/survey/1');
+
+ await expect(page.locator('body')).toContainText('Question');
+
+});
\ No newline at end of file
diff --git a/testing/question-update.spec.js b/testing/question-update.spec.js
new file mode 100644
index 00000000..655618d6
--- /dev/null
+++ b/testing/question-update.spec.js
@@ -0,0 +1,18 @@
+const { test, expect } = require('@playwright/test');
+
+test('Halaman Edit Pertanyaan', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/question/survey/1');
+
+ await page.locator('a[title="Edit Question"]').first().click();
+
+ await expect(page.locator('h1')).toContainText('Edit');
+
+});
\ No newline at end of file
diff --git a/testing/question.spec.js b/testing/question.spec.js
new file mode 100644
index 00000000..5d6c05d7
--- /dev/null
+++ b/testing/question.spec.js
@@ -0,0 +1,16 @@
+const { test, expect } = require('@playwright/test');
+
+test('Membuka halaman Pertanyaan', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/question/survey/1');
+
+ await expect(page.locator('h1')).toContainText('Question');
+
+});
\ No newline at end of file
diff --git a/testing/survey-api.spec.js b/testing/survey-api.spec.js
new file mode 100644
index 00000000..43cc79d1
--- /dev/null
+++ b/testing/survey-api.spec.js
@@ -0,0 +1,15 @@
+const { test, expect } = require('@playwright/test');
+
+test('REST API Survey', async ({ request }) => {
+
+ const response = await request.get(
+ 'http://localhost:3000/api/surveys'
+ );
+
+ expect(response.ok()).toBeTruthy();
+
+ const data = await response.json();
+
+ expect(Array.isArray(data)).toBeTruthy();
+
+});
\ No newline at end of file
diff --git a/testing/survey-delete.spec.js b/testing/survey-delete.spec.js
new file mode 100644
index 00000000..ecc587d8
--- /dev/null
+++ b/testing/survey-delete.spec.js
@@ -0,0 +1,18 @@
+const { test, expect } = require('@playwright/test');
+
+test('Halaman Survey memiliki tombol hapus', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]','hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]','hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/survey');
+
+ await expect(
+ page.locator('.action.delete').first()
+ ).toBeVisible();
+
+});
\ No newline at end of file
diff --git a/testing/survey-export-pdf.spec.js b/testing/survey-export-pdf.spec.js
new file mode 100644
index 00000000..9bb0ecc6
--- /dev/null
+++ b/testing/survey-export-pdf.spec.js
@@ -0,0 +1,25 @@
+const { test, expect } = require('@playwright/test');
+
+test('Export PDF Survey', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]','hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]','hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/survey');
+
+ await expect(
+ page.locator('.modern-table')
+ ).toBeVisible();
+
+ const [download] = await Promise.all([
+ page.waitForEvent('download'),
+ page.click('a[href="/survey/export/pdf"]')
+ ]);
+
+ expect(download.suggestedFilename().toLowerCase()).toContain('survey');
+
+});
\ No newline at end of file
diff --git a/testing/survey-list.spec.js b/testing/survey-list.spec.js
new file mode 100644
index 00000000..f5ae0da1
--- /dev/null
+++ b/testing/survey-list.spec.js
@@ -0,0 +1,16 @@
+const { test, expect } = require('@playwright/test');
+
+test('Menampilkan daftar survey', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.locator('button[type="submit"]').click();
+
+ await page.goto('http://localhost:3000/survey');
+
+ await expect(page.locator('h1')).toContainText('Survey');
+
+});
\ No newline at end of file
diff --git a/testing/survey-pagination.spec.js b/testing/survey-pagination.spec.js
new file mode 100644
index 00000000..609d6798
--- /dev/null
+++ b/testing/survey-pagination.spec.js
@@ -0,0 +1,18 @@
+const { test, expect } = require('@playwright/test');
+
+test('Pagination Survey tampil', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/survey');
+
+ await expect(
+ page.locator('.pagination')
+ ).toBeVisible();
+
+});
\ No newline at end of file
diff --git a/testing/survey-publish.spec.js b/testing/survey-publish.spec.js
new file mode 100644
index 00000000..00a320fe
--- /dev/null
+++ b/testing/survey-publish.spec.js
@@ -0,0 +1,24 @@
+const { test, expect } = require('@playwright/test');
+
+test('Tombol Publish Survey tampil', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/survey');
+
+ const publishButton = page.locator('form[action*="/publish/"] button');
+
+ const count = await publishButton.count();
+
+ if (count > 0) {
+ await expect(publishButton.first()).toBeVisible();
+ } else {
+ await expect(page.locator('body')).toContainText('Aktif');
+ }
+
+});
\ No newline at end of file
diff --git a/testing/survey-search.spec.js b/testing/survey-search.spec.js
new file mode 100644
index 00000000..e4a5ba87
--- /dev/null
+++ b/testing/survey-search.spec.js
@@ -0,0 +1,22 @@
+const { test, expect } = require('@playwright/test');
+
+test('Mencari Survey', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/survey');
+
+ await page.fill('input[name="search"]', 'Survey');
+
+ await page.click('button[type="submit"]');
+
+ await expect(
+ page.locator('.modern-table')
+ ).toBeVisible();
+
+});
\ No newline at end of file
diff --git a/testing/survey-update.spec.js b/testing/survey-update.spec.js
new file mode 100644
index 00000000..7132d4ff
--- /dev/null
+++ b/testing/survey-update.spec.js
@@ -0,0 +1,20 @@
+const { test, expect } = require('@playwright/test');
+
+test('Halaman Edit Survey', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/survey');
+
+ await page.locator('.action.edit').first().click();
+
+ await expect(
+ page.locator('h1')
+ ).toContainText('Edit');
+
+});
\ No newline at end of file
diff --git a/testing/validation.spec.js b/testing/validation.spec.js
new file mode 100644
index 00000000..5b6bacae
--- /dev/null
+++ b/testing/validation.spec.js
@@ -0,0 +1,18 @@
+const { test, expect } = require('@playwright/test');
+
+test('Validasi Form Survey', async ({ page }) => {
+
+ await page.goto('http://localhost:3000/login');
+
+ await page.fill('input[name="email"]', 'hanifalhaj@gmail.com');
+ await page.fill('input[name="password"]', 'hanif123');
+
+ await page.click('button[type="submit"]');
+
+ await page.goto('http://localhost:3000/survey/create');
+
+ await page.click('button[type="submit"]');
+
+ await expect(page).toHaveURL(/survey\/create/);
+
+});
\ No newline at end of file
diff --git a/views/assignment/create.ejs b/views/assignment/create.ejs
new file mode 100644
index 00000000..d5ebda6d
--- /dev/null
+++ b/views/assignment/create.ejs
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+<%= title %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%- include("../partials/sidebar") %>
+
+
+
+ <%- include("../partials/navbar") %>
+
+
+
+
+
+ <% if(error){ %>
+
+
+
+ <%= error %>
+
+
+
+ <% } %>
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/assignment/edit.ejs b/views/assignment/edit.ejs
new file mode 100644
index 00000000..a92cb5bf
--- /dev/null
+++ b/views/assignment/edit.ejs
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+<%= title %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%- include("../partials/sidebar") %>
+
+
+
+ <%- include("../partials/navbar") %>
+
+
+
+
+
+ <% if(error){ %>
+
+
+
+ <%= error %>
+
+
+
+ <% } %>
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/assignment/index.ejs b/views/assignment/index.ejs
new file mode 100644
index 00000000..f42c64b2
--- /dev/null
+++ b/views/assignment/index.ejs
@@ -0,0 +1,391 @@
+
+
+
+
+
+
+
+
+<%= title %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<%- include("../partials/sidebar") %>
+
+
+
+<%- include("../partials/navbar") %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🔗
+
+
+
+
+
+
Total Assignment
+
+
+
+ <%= assignments.length %>
+
+
+
+
+
+
+
+
+
+
+
+ 📄
+
+
+
+
+
+
Current Page
+
+
+
+ <%= page %>
+
+
+
+
+
+
+
+
+
+
+
+ 📚
+
+
+
+
+
+
Total Page
+
+
+
+ <%= totalPage %>
+
+
+
+
+
+
+
+
+
+
+
+ 🔍
+
+
+
+
+
+
Search
+
+
+
+ <%= search ? search : "-" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+| No |
+
+Survey |
+
+Question |
+
+Order |
+
+
+
+Action
+
+ |
+
+
+
+
+
+
+
+<% if(assignments.length===0){ %>
+
+
+
+
+
+
+
+ 📭 No Assignment
+
+
+
+No assignment available.
+
+
+
+
+
+ |
+
+
+
+<% } %>
+
+<% assignments.forEach((assignment,index)=>{ %>
+
+
+
+|
+
+<%= ((page-1)*5)+index+1 %>
+
+ |
+
+
+
+
+
+<%= assignment.survey_title %>
+
+
+
+ |
+
+
+
+<%= assignment.question_text %>
+
+ |
+
+
+
+
+
+<%= assignment.order %>
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+<% }) %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/home.ejs b/views/home.ejs
index b0f86047..063a6e17 100644
--- a/views/home.ejs
+++ b/views/home.ejs
@@ -1,1872 +1,390 @@
-
+
-
-
-
-
-
-
-
-
-
- Basecoat
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-