comment field for responses (closes #18)

This commit is contained in:
Markus Brueckner 2025-04-20 22:03:33 +02:00
parent 5b39c03a7d
commit dafa5e6b23
12 changed files with 684 additions and 178 deletions

View file

@ -0,0 +1,11 @@
CREATE TABLE `survey_comments_table` (
`id` int AUTO_INCREMENT NOT NULL,
`surveyId` int NOT NULL,
`participantId` int NOT NULL,
`comment` text NOT NULL,
CONSTRAINT `survey_comments_table_id` PRIMARY KEY(`id`)
);
--> statement-breakpoint
ALTER TABLE `survey_comments_table` ADD CONSTRAINT `survey_comments_table_surveyId_surveys_table_id_fk` FOREIGN KEY (`surveyId`) REFERENCES `surveys_table`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `survey_comments_table` ADD CONSTRAINT `survey_comments_table_participantId_survey_access_table_id_fk` FOREIGN KEY (`participantId`) REFERENCES `survey_access_table`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX `survey_participant_index` ON `survey_comments_table` (`surveyId`,`participantId`);

View file

@ -0,0 +1,556 @@
{
"version": "5",
"dialect": "mysql",
"id": "77ad797c-05e7-432e-bb86-b6a0bd22f71a",
"prevId": "4d29fb1a-427c-4082-a176-08688a6d9f2e",
"tables": {
"sessions": {
"name": "sessions",
"columns": {
"token": {
"name": "token",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires": {
"name": "expires",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_userId_users_table_id_fk": {
"name": "sessions_userId_users_table_id_fk",
"tableFrom": "sessions",
"tableTo": "users_table",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"sessions_token": {
"name": "sessions_token",
"columns": [
"token"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"survey_access_table": {
"name": "survey_access_table",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"surveyId": {
"name": "surveyId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"recepientEmail": {
"name": "recepientEmail",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"accessToken": {
"name": "accessToken",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"token_index": {
"name": "token_index",
"columns": [
"accessToken"
],
"isUnique": false
}
},
"foreignKeys": {
"survey_access_table_surveyId_surveys_table_id_fk": {
"name": "survey_access_table_surveyId_surveys_table_id_fk",
"tableFrom": "survey_access_table",
"tableTo": "surveys_table",
"columnsFrom": [
"surveyId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"survey_access_table_id": {
"name": "survey_access_table_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"survey_answers_table": {
"name": "survey_answers_table",
"columns": {
"participantId": {
"name": "participantId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"skillId": {
"name": "skillId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"rating": {
"name": "rating",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"survey_answers_table_participantId_survey_access_table_id_fk": {
"name": "survey_answers_table_participantId_survey_access_table_id_fk",
"tableFrom": "survey_answers_table",
"tableTo": "survey_access_table",
"columnsFrom": [
"participantId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"survey_answers_table_skillId_survey_skills_table_id_fk": {
"name": "survey_answers_table_skillId_survey_skills_table_id_fk",
"tableFrom": "survey_answers_table",
"tableTo": "survey_skills_table",
"columnsFrom": [
"skillId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"survey_answers_table_participantId_skillId_pk": {
"name": "survey_answers_table_participantId_skillId_pk",
"columns": [
"participantId",
"skillId"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"survey_comments_table": {
"name": "survey_comments_table",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"surveyId": {
"name": "surveyId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"participantId": {
"name": "participantId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"survey_participant_index": {
"name": "survey_participant_index",
"columns": [
"surveyId",
"participantId"
],
"isUnique": false
}
},
"foreignKeys": {
"survey_comments_table_surveyId_surveys_table_id_fk": {
"name": "survey_comments_table_surveyId_surveys_table_id_fk",
"tableFrom": "survey_comments_table",
"tableTo": "surveys_table",
"columnsFrom": [
"surveyId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"survey_comments_table_participantId_survey_access_table_id_fk": {
"name": "survey_comments_table_participantId_survey_access_table_id_fk",
"tableFrom": "survey_comments_table",
"tableTo": "survey_access_table",
"columnsFrom": [
"participantId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"survey_comments_table_id": {
"name": "survey_comments_table_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"survey_permissions_table": {
"name": "survey_permissions_table",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"surveyId": {
"name": "surveyId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user": {
"name": "user",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access": {
"name": "access",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_index": {
"name": "user_index",
"columns": [
"user"
],
"isUnique": false
},
"survey_index": {
"name": "survey_index",
"columns": [
"surveyId"
],
"isUnique": false
}
},
"foreignKeys": {
"survey_permissions_table_surveyId_surveys_table_id_fk": {
"name": "survey_permissions_table_surveyId_surveys_table_id_fk",
"tableFrom": "survey_permissions_table",
"tableTo": "surveys_table",
"columnsFrom": [
"surveyId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"survey_permissions_table_user_users_table_id_fk": {
"name": "survey_permissions_table_user_users_table_id_fk",
"tableFrom": "survey_permissions_table",
"tableTo": "users_table",
"columnsFrom": [
"user"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"survey_permissions_table_id": {
"name": "survey_permissions_table_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"survey_user_un": {
"name": "survey_user_un",
"columns": [
"surveyId",
"user"
]
}
},
"checkConstraint": {}
},
"survey_skills_table": {
"name": "survey_skills_table",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"surveyId": {
"name": "surveyId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"survey_skills_table_surveyId_surveys_table_id_fk": {
"name": "survey_skills_table_surveyId_surveys_table_id_fk",
"tableFrom": "survey_skills_table",
"tableTo": "surveys_table",
"columnsFrom": [
"surveyId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"survey_skills_table_id": {
"name": "survey_skills_table_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"surveys_table": {
"name": "surveys_table",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"owner": {
"name": "owner",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"surveys_table_owner_users_table_id_fk": {
"name": "surveys_table_owner_users_table_id_fk",
"tableFrom": "surveys_table",
"tableTo": "users_table",
"columnsFrom": [
"owner"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"surveys_table_id": {
"name": "surveys_table_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"users_table": {
"name": "users_table",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"verification_code": {
"name": "verification_code",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"verifcationCodeExpires": {
"name": "verifcationCodeExpires",
"type": "date",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_table_id": {
"name": "users_table_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_table_email_unique": {
"name": "users_table_email_unique",
"columns": [
"email"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View file

@ -29,6 +29,13 @@
"when": 1736628358111, "when": 1736628358111,
"tag": "0003_overjoyed_shooting_star", "tag": "0003_overjoyed_shooting_star",
"breakpoints": true "breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1745216339779,
"tag": "0004_motionless_havok",
"breakpoints": true
} }
] ]
} }

227
package-lock.json generated
View file

@ -20,7 +20,7 @@
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"debug": "^4.4.0", "debug": "^4.4.0",
"drizzle-orm": "^0.36.4", "drizzle-orm": "^0.36.4",
"layerchart": "^0.59.1", "layerchart": "^1.0.8",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"mysql2": "^3.11.4", "mysql2": "^3.11.4",
"nodemailer": "^6.9.16" "nodemailer": "^6.9.16"
@ -81,6 +81,24 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@dagrejs/dagre": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz",
"integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==",
"license": "MIT",
"dependencies": {
"@dagrejs/graphlib": "2.2.4"
}
},
"node_modules/@dagrejs/graphlib": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
"integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
"license": "MIT",
"engines": {
"node": ">17.0.0"
}
},
"node_modules/@drizzle-team/brocli": { "node_modules/@drizzle-team/brocli": {
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
@ -1096,19 +1114,19 @@
} }
}, },
"node_modules/@floating-ui/dom": { "node_modules/@floating-ui/dom": {
"version": "1.6.12", "version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/core": "^1.6.0", "@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.8" "@floating-ui/utils": "^0.2.9"
} }
}, },
"node_modules/@floating-ui/utils": { "node_modules/@floating-ui/utils": {
"version": "0.2.8", "version": "0.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
@ -1253,13 +1271,13 @@
} }
}, },
"node_modules/@layerstack/svelte-actions": { "node_modules/@layerstack/svelte-actions": {
"version": "0.0.9", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@layerstack/svelte-actions/-/svelte-actions-0.0.9.tgz", "resolved": "https://registry.npmjs.org/@layerstack/svelte-actions/-/svelte-actions-1.0.0.tgz",
"integrity": "sha512-1mDXM7R0pT3pvQvXS479hN9tkn6M812Iip9M7BSorDTQcccGNVGU67aqLO5qded7CuzRnyeN2id5KcVU5zfY8A==", "integrity": "sha512-cnVxMbcc+xYta9hwHnLja4a7bR7cfX/QW9jDY4ZCI8RIt+3lKNRE+u2XzWoGp4Qsx7A0/nGUIiLWpDBgO1ihsQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/dom": "^1.6.12", "@floating-ui/dom": "^1.6.13",
"@layerstack/utils": "0.0.7", "@layerstack/utils": "1.0.0",
"d3-array": "^3.2.4", "d3-array": "^3.2.4",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@ -1267,26 +1285,26 @@
} }
}, },
"node_modules/@layerstack/svelte-stores": { "node_modules/@layerstack/svelte-stores": {
"version": "0.0.9", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@layerstack/svelte-stores/-/svelte-stores-0.0.9.tgz", "resolved": "https://registry.npmjs.org/@layerstack/svelte-stores/-/svelte-stores-1.0.1.tgz",
"integrity": "sha512-5yyi7eV/2hOraw7wQgEObVR4s2/4T3G/GjGaZLR9BQH8mnsLuXw1n3k6rFT8NxiRvpEjWJ9l9ILOGc/XzFJb3g==", "integrity": "sha512-GVm0iMNJ8pI2q8WjofwejK+m6uF7PFUqgZZfkgDv2UmVh/HyW6ZpxA2wXzIxZley4NtUlrRw8cpyy2sJOEGVSA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@layerstack/utils": "0.0.7", "@layerstack/utils": "1.0.0",
"d3-array": "^3.2.4", "d3-array": "^3.2.4",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"immer": "^10.1.1", "immer": "^10.1.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"zod": "^3.23.8" "zod": "^3.24.2"
} }
}, },
"node_modules/@layerstack/tailwind": { "node_modules/@layerstack/tailwind": {
"version": "0.0.11", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@layerstack/tailwind/-/tailwind-0.0.11.tgz", "resolved": "https://registry.npmjs.org/@layerstack/tailwind/-/tailwind-1.0.0.tgz",
"integrity": "sha512-dHvJM5xpVisPrM6NClSCNPqB/u+pSwemWzkKeb911/8pLUuyr+7HceTpv5AZMyE/I5A2RxoTa5mTZokmvEqPnQ==", "integrity": "sha512-xV2M6KRNBmbhV9XAvTHiw/BJSTIh5eby+Hj1iXXkMr88fObZHn4s1G2BILEDbdwjZl6Ivu1Du0gO9Vz3lzflUA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@layerstack/utils": "^0.0.7", "@layerstack/utils": "^1.0.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"culori": "^4.0.1", "culori": "^4.0.1",
"d3-array": "^3.2.4", "d3-array": "^3.2.4",
@ -1297,9 +1315,9 @@
} }
}, },
"node_modules/@layerstack/utils": { "node_modules/@layerstack/utils": {
"version": "0.0.7", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@layerstack/utils/-/utils-0.0.7.tgz", "resolved": "https://registry.npmjs.org/@layerstack/utils/-/utils-1.0.0.tgz",
"integrity": "sha512-7R9AOw/J3TkvSCuQXajK28d7AKuvsEgMQlXBSvAr9tcxSrb9hzraX47uD4uEmEAIB9IiKaG4IUx2FmGiJ3WlTQ==", "integrity": "sha512-kIH8lo4MEzPGtdxQRacy8RsSVhMQwe62ckuev62xQDE5QcUJ4wB9EMaRWaQK8CfEOpUERGPgkMPMbMqPhdp0gw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"d3-array": "^3.2.4", "d3-array": "^3.2.4",
@ -2865,12 +2883,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/array-source": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/array-source/-/array-source-0.0.4.tgz",
"integrity": "sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw==",
"license": "BSD-3-Clause"
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.20", "version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@ -3141,17 +3153,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/core-js": {
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -4197,12 +4198,6 @@
"node": "^12.20 || >= 14.13" "node": "^12.20 || >= 14.13"
} }
}, },
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"license": "MIT"
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@ -4216,15 +4211,6 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/file-source": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/file-source/-/file-source-0.6.1.tgz",
"integrity": "sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==",
"license": "BSD-3-Clause",
"dependencies": {
"stream-source": "0.3"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -4766,15 +4752,16 @@
} }
}, },
"node_modules/layerchart": { "node_modules/layerchart": {
"version": "0.59.6", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/layerchart/-/layerchart-0.59.6.tgz", "resolved": "https://registry.npmjs.org/layerchart/-/layerchart-1.0.8.tgz",
"integrity": "sha512-P7ed+YDykJnoMFqDh/npE8SJWIiMvoro85sJx+hglEYmOro0JldW78BRXl1UNNh04W42OS+Ky/kGTTPI0Pm22Q==", "integrity": "sha512-JkSNLUFiTkUYOmJclnM97oO/xtr/YuF4bnAf1ySz2ZeoS97HNUgWjIcYl5JkNrF112VA1K/ODranx5ZbYpXxqA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@layerstack/svelte-actions": "^0.0.9", "@dagrejs/dagre": "^1.1.4",
"@layerstack/svelte-stores": "^0.0.9", "@layerstack/svelte-actions": "^1.0.0",
"@layerstack/tailwind": "^0.0.11", "@layerstack/svelte-stores": "^1.0.0",
"@layerstack/utils": "^0.0.7", "@layerstack/tailwind": "^1.0.0",
"@layerstack/utils": "^1.0.0",
"d3-array": "^3.2.4", "d3-array": "^3.2.4",
"d3-color": "^3.1.0", "d3-color": "^3.1.0",
"d3-delaunay": "^6.0.4", "d3-delaunay": "^6.0.4",
@ -4796,10 +4783,7 @@
"d3-time": "^3.1.0", "d3-time": "^3.1.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"layercake": "^8.4.2", "layercake": "^8.4.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21"
"posthog-js": "^1.187.2",
"shapefile": "^0.6.6",
"topojson-client": "^3.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"svelte": "^3.56.0 || ^4.0.0 || ^5.0.0" "svelte": "^3.56.0 || ^4.0.0 || ^5.0.0"
@ -5346,16 +5330,6 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/path-source": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/path-source/-/path-source-0.1.3.tgz",
"integrity": "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==",
"license": "BSD-3-Clause",
"dependencies": {
"array-source": "0.0",
"file-source": "0.6"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -5588,28 +5562,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/posthog-js": {
"version": "1.203.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.203.1.tgz",
"integrity": "sha512-r/WiSyz6VNbIKEV/30+aD5gdrYkFtmZwvqNa6h9frl8hG638v098FrXaq3EYzMcCdkQf3phaZTDIAFKegpiTjw==",
"license": "MIT",
"dependencies": {
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
"web-vitals": "^4.2.0"
}
},
"node_modules/preact": {
"version": "10.25.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.25.3.tgz",
"integrity": "sha512-dzQmIFtM970z+fP9ziQ3yG4e3ULIbwZzJ734vaMVUTaKQ2+Ru1Ou/gjshOYVHCcd1rpAelC6ngjvjDXph98unQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -5962,30 +5914,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/shapefile": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/shapefile/-/shapefile-0.6.6.tgz",
"integrity": "sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==",
"license": "BSD-3-Clause",
"dependencies": {
"array-source": "0.0",
"commander": "2",
"path-source": "0.1",
"slice-source": "0.4",
"stream-source": "0.3",
"text-encoding": "^0.6.4"
},
"bin": {
"dbf2json": "bin/dbf2json",
"shp2json": "bin/shp2json"
}
},
"node_modules/shapefile/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -6034,12 +5962,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/slice-source": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/slice-source/-/slice-source-0.4.1.tgz",
"integrity": "sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==",
"license": "BSD-3-Clause"
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -6079,12 +6001,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/stream-source": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz",
"integrity": "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g==",
"license": "BSD-3-Clause"
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -6569,13 +6485,6 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/text-encoding": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
"integrity": "sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==",
"deprecated": "no longer maintained",
"license": "Unlicense"
},
"node_modules/thenify": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -6620,26 +6529,6 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/topojson-client": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
"license": "ISC",
"dependencies": {
"commander": "2"
},
"bin": {
"topo2geo": "bin/topo2geo",
"topomerge": "bin/topomerge",
"topoquantize": "bin/topoquantize"
}
},
"node_modules/topojson-client/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/totalist": { "node_modules/totalist": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@ -7302,12 +7191,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
"license": "Apache-2.0"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -7472,9 +7355,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.24.1", "version": "3.24.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View file

@ -54,7 +54,7 @@
"d3-shape": "^3.2.0", "d3-shape": "^3.2.0",
"debug": "^4.4.0", "debug": "^4.4.0",
"drizzle-orm": "^0.36.4", "drizzle-orm": "^0.36.4",
"layerchart": "^0.59.1", "layerchart": "^1.0.8",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"mysql2": "^3.11.4", "mysql2": "^3.11.4",
"nodemailer": "^6.9.16" "nodemailer": "^6.9.16"

View file

@ -45,6 +45,15 @@ export const surveyAnswersTable = mysqlTable("survey_answers_table", {
pk: primaryKey({ columns: [table.participantId, table.skillId] }), pk: primaryKey({ columns: [table.participantId, table.skillId] }),
})); }));
export const surveyCommentsTable = mysqlTable("survey_comments_table", {
id: int().autoincrement().primaryKey(),
surveyId: int().notNull().references(() => surveysTable.id, { onDelete: "cascade" }),
participantId: int().notNull().references(() => surveyAccessTable.id, { onDelete: "cascade" }),
comment: text().notNull(),
}, (table) => ({
surveyParticipantIndex: index("survey_participant_index").on(table.surveyId, table.participantId),
}));
export const surveyPermissionsTable = mysqlTable("survey_permissions_table", { export const surveyPermissionsTable = mysqlTable("survey_permissions_table", {
id: int().autoincrement().primaryKey(), id: int().autoincrement().primaryKey(),
surveyId: int().notNull().references(() => surveysTable.id, { onDelete: "cascade" }), surveyId: int().notNull().references(() => surveysTable.id, { onDelete: "cascade" }),

View file

@ -5,7 +5,7 @@ const log = debug('survey:admin:edit');
import { eq, or, and, inArray, gte, sql, isNull } from "drizzle-orm"; import { eq, or, and, inArray, gte, sql, isNull } from "drizzle-orm";
import { db } from "../db"; import { db } from "../db";
import { surveyAccessTable, surveyAnswersTable, surveyPermissionsTable, surveySkillsTable, surveysTable, usersTable } from "../db/schema"; import { surveyAccessTable, surveyAnswersTable, surveyCommentsTable, surveyPermissionsTable, surveySkillsTable, surveysTable, usersTable } from "../db/schema";
import { AccessLevel, type AccessToken, type Email, type ParticipantId, type SkillId, type SurveyData, type SurveyId, type SurveyMetaData, type UserId } from "$lib/types"; import { AccessLevel, type AccessToken, type Email, type ParticipantId, type SkillId, type SurveyData, type SurveyId, type SurveyMetaData, type UserId } from "$lib/types";
import { checkAccess, whenAccess } from "$lib/helpers/shared/permissions"; import { checkAccess, whenAccess } from "$lib/helpers/shared/permissions";
import { generateRandomToken } from "$lib/helpers/shared/randomToken"; import { generateRandomToken } from "$lib/helpers/shared/randomToken";
@ -125,6 +125,7 @@ export async function loadSurvey(id: SurveyId, userId: UserId): Promise<SurveyDa
} }
return null; return null;
}); });
const comments = await whenAccess(survey, AccessLevel.ReadResult, () => db.select().from(surveyCommentsTable).where(eq(surveyCommentsTable.surveyId, id)));
return { return {
...survey, ...survey,
@ -135,7 +136,8 @@ export async function loadSurvey(id: SurveyId, userId: UserId): Promise<SurveyDa
answers: answers?.filter(answer => answer.participantId === participant.id).map(answer => ({ answers: answers?.filter(answer => answer.participantId === participant.id).map(answer => ({
skillId: answer.skillId as SkillId, skillId: answer.skillId as SkillId,
rating: answer.rating / 10 // convert back to original vallue. The DB stores integer values only to prevent rounding and matching errors rating: answer.rating / 10 // convert back to original vallue. The DB stores integer values only to prevent rounding and matching errors
})) ?? [] })) ?? [],
comment: comments?.find(comment => comment.participantId === participant.id)?.comment ?? null
})) ?? [], })) ?? [],
skills: skills?.map(skill => ({ skills: skills?.map(skill => ({
id: skill.id as SkillId, id: skill.id as SkillId,

View file

@ -0,0 +1,21 @@
<script lang="ts">
export type CommentEntry = {
email: string;
comment: string | null;
};
type Props = {
comments: CommentEntry[];
}
let { comments }: Props = $props();
</script>
<div class="m-4 border p-2">
{#each comments.filter((participant) => !!participant.comment) as comment}
<h3 class="text-lg">{comment.email}:</h3>
<p class="text-sm ml-4 mt-2">{comment.comment}</p>
{:else}
No comments
{/each}
</div>

View file

@ -55,6 +55,7 @@ export type SurveyParticipant = {
email: Email; email: Email;
accessToken: AccessToken | null; accessToken: AccessToken | null;
answers: SurveyAnswer[]; answers: SurveyAnswer[];
comment: string | null;
} }
/// Information about a specific skill /// Information about a specific skill

View file

@ -20,6 +20,7 @@
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import EyeSlash from '$lib/components/icons/EyeSlash.svelte'; import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
import Eye from '$lib/components/icons/Eye.svelte'; import Eye from '$lib/components/icons/Eye.svelte';
import Comments from '$lib/components/Comments.svelte';
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@ -164,6 +165,8 @@
<div class="grid grid-cols-1 justify-items-center"> <div class="grid grid-cols-1 justify-items-center">
<Diagram data={diagramData} {reviewMode}/> <Diagram data={diagramData} {reviewMode}/>
</div> </div>
<h2 class="ml-2 mt-4 text-2xl">Comments</h2>
<Comments comments={data.participants} />
{:else} {:else}
<div class="text-center">You do not have permission to see the survey results</div> <div class="text-center">You do not have permission to see the survey results</div>
{/if} {/if}

View file

@ -1,6 +1,6 @@
import { error, redirect } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import { db } from '../../db'; import { db } from '../../db';
import { surveyAccessTable, surveyAnswersTable, surveySkillsTable, surveysTable } from '../../db/schema'; import { surveyAccessTable, surveyAnswersTable, surveyCommentsTable, surveySkillsTable, surveysTable } from '../../db/schema';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
@ -65,13 +65,13 @@ export const actions = {
const formData = await request.formData(); const formData = await request.formData();
// validate that the form doesn't contain invalid skill IDs // validate that the form doesn't contain invalid skill IDs
const skillEntries = [...formData.entries()].filter(([key, _]) => !key.startsWith('disable-')); const skillEntries = [...formData.entries()].filter(([key, _]) => !key.startsWith('disable-') && key !== 'comment');
const allIdsValid = skillEntries.every(([key, _]) => skills.some(skill => skill.id.toString() === key)); const allIdsValid = skillEntries.every(([key, _]) => skills.some(skill => skill.id.toString() === key));
const deselectedIds = [...formData.entries().filter(([key, _]) => key.startsWith('disable-')).map(([key, _]) => parseInt(key.replace('disable-', '')))]; const deselectedIds = [...formData.entries().filter(([key, _]) => key.startsWith('disable-')).map(([key, _]) => parseInt(key.replace('disable-', '')))];
if (!allIdsValid || skillEntries.length + deselectedIds.length !== skills.length) { if (!allIdsValid || skillEntries.length + deselectedIds.length !== skills.length) {
let missing_ids = skills.filter(skill => !skillEntries.some(([key, _]) => key === skill.id.toString()) && !deselectedIds.includes(skill.id)); let missing_ids = skills.filter(skill => !skillEntries.some(([key, _]) => key === skill.id.toString()) && !deselectedIds.includes(skill.id));
let invalid_ids = skillEntries.filter(([key, _]) => !skills.some(skill => skill.id.toString() === key)).map(([key, _]) => key); let invalid_ids = skillEntries.filter(([key, _]) => !skills.some(skill => skill.id.toString() === key)).map(([key, _]) => key);
log_store.log("Invalid (%o) or missing (%o) skill IDs", invalid_ids, missing_ids); log_store("Invalid (%o) or missing (%o) skill IDs", invalid_ids, missing_ids);
error(400, 'Invalid skill ID'); error(400, 'Invalid skill ID');
} }
@ -90,6 +90,14 @@ export const actions = {
await db.insert(surveyAnswersTable).values([...answers]); await db.insert(surveyAnswersTable).values([...answers]);
const comment = formData.get('comment');
if (comment) {
await db.insert(surveyCommentsTable).values({
surveyId: survey.id, participantId: results[0].survey_access_table.id,
comment: comment.toString()
});
}
redirect(303, `/${params.accessToken}/thanks`); redirect(303, `/${params.accessToken}/thanks`);
} }
} }

View file

@ -26,6 +26,11 @@
<form method="POST" class="text-center"> <form method="POST" class="text-center">
<Skills bind:skills /> <Skills bind:skills />
<label for="comment" class="mt-5 w-1/2 block m-auto">
<p>Please provide additional comments, if necessary.</p>
</label>
<textarea name="comment" id="comment" class="mt-5 w-1/2 block m-auto" placeholder="Comment"></textarea>
<button type="submit" class="mt-5 w-40 bg-slate-200">Submit</button> <button type="submit" class="mt-5 w-40 bg-slate-200">Submit</button>
</form> </form>