diff --git a/migrations/versions/009_add_notes_to_onboarding_process.py b/migrations/versions/009_add_notes_to_onboarding_process.py new file mode 100644 index 0000000..2921de6 --- /dev/null +++ b/migrations/versions/009_add_notes_to_onboarding_process.py @@ -0,0 +1,26 @@ +"""add_notes_to_onboarding_process + +Adds a nullable `notes` text column to the onboarding_process table. + +Revision ID: 009 +Revises: 008 +Create Date: 2026-05-13 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '009' +down_revision = '008' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column('onboarding_process', sa.Column('notes', sa.Text(), nullable=True)) + + +def downgrade(): + op.drop_column('onboarding_process', 'notes') diff --git a/package-lock.json b/package-lock.json index 458be01..bc873b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,16 +12,16 @@ "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.8", "chart.js": "^4.5.1", - "easymde": "^2.20.0", + "easymde": "^2.21.0", "fullcalendar": "^6.1.20", "html2canvas": "^1.4.1", - "mermaid": "^11.14.0", + "mermaid": "^11.15.0", "orgchart.js": "^0.0.4", "simple-datatables": "^10.2.0", "sortablejs": "^1.15.7", - "suneditor": "^3.1.2", - "swagger-ui-dist": "5.32.5", - "tom-select": "^2.6.0" + "suneditor": "^3.1.3", + "swagger-ui-dist": "5.32.6", + "tom-select": "^2.6.1" } }, "node_modules/@antfu/install-pkg": { @@ -41,42 +41,10 @@ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==" }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", - "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "12.0.0", - "@chevrotain/types": "12.0.0" - } - }, - "node_modules/@chevrotain/gast": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", - "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "12.0.0" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", - "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", - "license": "Apache-2.0" - }, "node_modules/@chevrotain/types": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", - "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", - "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", - "license": "Apache-2.0" + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==" }, "node_modules/@fortawesome/fontawesome-free": { "version": "7.2.0", @@ -146,13 +114,13 @@ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==" }, "node_modules/@iconify/utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", - "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==", "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", - "mlly": "^1.8.0" + "import-meta-resolve": "^4.2.0" } }, "node_modules/@kurkle/color": { @@ -161,12 +129,11 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" }, "node_modules/@mermaid-js/parser": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", - "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", - "license": "MIT", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz", + "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==", "dependencies": { - "langium": "^4.0.0" + "@chevrotain/types": "~11.1.1" } }, "node_modules/@orchidjs/sifter": { @@ -428,9 +395,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" }, "node_modules/@types/geojson": { "version": "7946.0.16", @@ -465,17 +432,6 @@ "d3-transition": "^3.0.1" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -513,34 +469,6 @@ "pnpm": ">=8" } }, - "node_modules/chevrotain": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", - "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "12.0.0", - "@chevrotain/gast": "12.0.0", - "@chevrotain/regexp-to-ast": "12.0.0", - "@chevrotain/types": "12.0.0", - "@chevrotain/utils": "12.0.0" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/chevrotain-allstar": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz", - "integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==", - "license": "MIT", - "dependencies": { - "lodash-es": "^4.17.21" - }, - "peerDependencies": { - "chevrotain": "^12.0.0" - } - }, "node_modules/codemirror": { "version": "5.65.21", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.21.tgz", @@ -562,11 +490,6 @@ "node": ">= 10" } }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==" - }, "node_modules/cose-base": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", @@ -584,9 +507,9 @@ } }, "node_modules/cytoscape": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", - "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "version": "3.33.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.3.tgz", + "integrity": "sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==", "engines": { "node": ">=0.10" } @@ -1046,9 +969,9 @@ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==" }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "dependencies": { "robust-predicates": "^3.0.2" } @@ -1059,18 +982,17 @@ "integrity": "sha512-G6WquwIa/XOxiYDYRMxfJj+c+a6LXMv7zgXoAQ3eRjTYkc8Lc4Aq97krTu/B15r243FR7dyeSxDenzKtO6sbfA==" }, "node_modules/dompurify": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", - "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", - "license": "(MPL-2.0 OR Apache-2.0)", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.3.tgz", + "integrity": "sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "node_modules/easymde": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.20.0.tgz", - "integrity": "sha512-V1Z5f92TfR42Na852OWnIZMbM7zotWQYTddNaLYZFVKj7APBbyZ3FYJ27gBw2grMW3R6Qdv9J8n5Ij7XRSIgXQ==", + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.21.0.tgz", + "integrity": "sha512-5uE7I/DEN8gvGRwxaqAv7h1PMEK2ykNXVX5zL0dK3nCYROGja3AMbdQz8eCEELnfvCfy7tRkTmLuvyJG8uSWjQ==", "dependencies": { "@types/codemirror": "^5.60.10", "@types/marked": "^4.0.7", @@ -1079,6 +1001,11 @@ "marked": "^4.1.0" } }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==" + }, "node_modules/font-awesome": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", @@ -1128,6 +1055,15 @@ "node": ">=0.10.0" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -1137,9 +1073,9 @@ } }, "node_modules/katex": { - "version": "0.16.38", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.38.tgz", - "integrity": "sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==", + "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -1164,24 +1100,6 @@ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" }, - "node_modules/langium": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz", - "integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==", - "license": "MIT", - "dependencies": { - "@chevrotain/regexp-to-ast": "~12.0.0", - "chevrotain": "~12.0.0", - "chevrotain-allstar": "~0.4.1", - "vscode-languageserver": "~9.0.1", - "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.1.0" - }, - "engines": { - "node": ">=20.10.0", - "npm": ">=10.2.3" - } - }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", @@ -1190,8 +1108,7 @@ "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==" }, "node_modules/marked": { "version": "4.3.0", @@ -1205,14 +1122,13 @@ } }, "node_modules/mermaid": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", - "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", - "license": "MIT", + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", + "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^1.1.0", + "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", @@ -1223,14 +1139,14 @@ "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", + "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", - "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "node_modules/mermaid/node_modules/marked": { @@ -1244,17 +1160,6 @@ "node": ">= 20" } }, - "node_modules/mlly": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", - "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, "node_modules/mockjs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mockjs/-/mockjs-1.1.0.tgz", @@ -1294,21 +1199,6 @@ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==" }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", @@ -1333,9 +1223,9 @@ } }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==" }, "node_modules/roughjs": { "version": "4.6.6", @@ -1373,24 +1263,22 @@ "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==" }, "node_modules/stylis": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==" + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==" }, "node_modules/suneditor": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/suneditor/-/suneditor-3.1.2.tgz", - "integrity": "sha512-eQSqVesH6a09uRdU+0CoWvHe4Zmhmyc4XxBHeaVI7fLe7W7XodcNdUGx3KtexoRNPE1SW8riQQkm46sWLa+cJQ==", - "license": "MIT", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/suneditor/-/suneditor-3.1.3.tgz", + "integrity": "sha512-OdhGXc83OlUBJtjk1rOrkVZmFgxjUcIsKf3gU8id1Wh7Jm8RHkx4vutQ4n6Nb4AXVmsr5p7ZKIB2fpag/hMXjA==", "engines": { "node": ">=14.0.0" } }, "node_modules/swagger-ui-dist": { - "version": "5.32.5", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.5.tgz", - "integrity": "sha512-7/FQfWe9A4qoyYFdAwy0chD0uDYidDp/ZT9VQ9LZlgD4AnnHJk8/+ytAA1HkJYOPySmK6helPDdJQMlcumt7HA==", - "license": "Apache-2.0", + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", "dependencies": { "@scarf/scarf": "=1.4.0" } @@ -1404,18 +1292,17 @@ } }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "engines": { "node": ">=18" } }, "node_modules/tom-select": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.6.0.tgz", - "integrity": "sha512-o2ToBjhUAnrrQvW/hrY9c//TpOpAKYSlfuFnf0DIwNy+ua+mmYnsF4PxN/PpzBfUIfEFkNYAngeGBfOAZWF3tw==", - "license": "Apache-2.0", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.6.1.tgz", + "integrity": "sha512-d/1kngVOQTGcI/2pVDfDLYjtjUgSSd3fSgkYUpi0y+yRtQQu2kzljj3aUdqMfqc45cjPvDEpfDt/hSX4awDFTg==", "dependencies": { "@orchidjs/sifter": "^1.1.0", "@orchidjs/unicode-variants": "^1.1.2" @@ -1437,14 +1324,9 @@ } }, "node_modules/typo-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.3.1.tgz", - "integrity": "sha512-elJkpCL6Z77Ghw0Lv0lGnhBAjSTOQ5FhiVOCfOuxhaoTT2xtLVbqikYItK5HHchzPbHEUFAcjOH669T2ZzeCbg==" - }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.3.2.tgz", + "integrity": "sha512-Z1YkJ7IIYNrFeOxAlHUercY4Q2I+PhYD/3VkWpJGy/Oqudy3bFpNcQxnv6Oa9fTSXCHPGz1eDoX1bZYm2Z891A==" }, "node_modules/utrie": { "version": "1.0.2", @@ -1455,65 +1337,16 @@ } }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" + "uuid": "dist-node/bin/uuid" } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "license": "MIT" } } } diff --git a/package.json b/package.json index c62365a..69705d2 100644 --- a/package.json +++ b/package.json @@ -11,15 +11,15 @@ "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.8", "chart.js": "^4.5.1", - "easymde": "^2.20.0", + "easymde": "^2.21.0", "fullcalendar": "^6.1.20", "html2canvas": "^1.4.1", - "mermaid": "^11.14.0", + "mermaid": "^11.15.0", "orgchart.js": "^0.0.4", "simple-datatables": "^10.2.0", "sortablejs": "^1.15.7", - "suneditor": "^3.1.2", - "swagger-ui-dist": "5.32.5", - "tom-select": "^2.6.0" + "suneditor": "^3.1.3", + "swagger-ui-dist": "5.32.6", + "tom-select": "^2.6.1" } } diff --git a/src/__init__.py b/src/__init__.py index b972e02..85efb41 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -275,16 +275,14 @@ def nl2br_filter(s): from .routes.attachments import attachments_bp from .routes.treeview import treeview_bp from .routes.admin import admin_bp - from .routes.opportunities import opportunities_bp - from .routes.evaluations import evaluations_bp # New: renamed from opportunities + from .routes.evaluations import evaluations_bp from .routes.policies import policies_bp from .routes.compliance import compliance_bp from .routes.risk import risk_bp from .routes.training import training_bp from .routes.maintenance import maintenance_bp from .routes.disposal import disposal_bp - from .routes.leads import leads_bp - from .routes.requirements import requirements_bp # New: renamed from leads + from .routes.requirements import requirements_bp from .routes.documentation import documentation_bp from .routes.frameworks import frameworks_bp from .routes.links import links_bp @@ -319,8 +317,7 @@ def favicon(): app.register_blueprint(attachments_bp, url_prefix='/attachments') app.register_blueprint(treeview_bp, url_prefix='/tree-view') app.register_blueprint(admin_bp, url_prefix='/admin') - app.register_blueprint(opportunities_bp, url_prefix='/opportunities') # Legacy - kept for backward compatibility - app.register_blueprint(evaluations_bp) # New: /evaluations (includes prefix in blueprint) + app.register_blueprint(evaluations_bp) app.register_blueprint(policies_bp, url_prefix='/policies') app.register_blueprint(compliance_bp, url_prefix='/compliance') app.register_blueprint(risk_bp, url_prefix='/risk') @@ -330,8 +327,7 @@ def favicon(): app.register_blueprint(training_bp, url_prefix='/training') app.register_blueprint(maintenance_bp) app.register_blueprint(disposal_bp) - app.register_blueprint(leads_bp) # Legacy - kept for backward compatibility - app.register_blueprint(requirements_bp) # New: /requirements (includes prefix in blueprint) + app.register_blueprint(requirements_bp) app.register_blueprint(documentation_bp, url_prefix='/documentation') app.register_blueprint(frameworks_bp) diff --git a/src/models/onboarding.py b/src/models/onboarding.py index 84b9c6b..1095037 100644 --- a/src/models/onboarding.py +++ b/src/models/onboarding.py @@ -59,6 +59,7 @@ class OnboardingProcess(db.Model): personal_email = db.Column(db.String(120), nullable=True) start_date = db.Column(db.Date, nullable=False) + notes = db.Column(db.Text, nullable=True) status = db.Column(db.String(50), default='Provisioning') # Provisioning, Completed pack_id = db.Column(db.Integer, db.ForeignKey('onboarding_pack.id')) diff --git a/src/routes/leads.py b/src/routes/leads.py deleted file mode 100644 index 530056c..0000000 --- a/src/routes/leads.py +++ /dev/null @@ -1,94 +0,0 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash -from ..models import db, Lead, Opportunity, Supplier -from .main import login_required -from ..services.permissions_service import requires_permission, has_write_permission - -leads_bp = Blueprint('leads', __name__, url_prefix='/leads') - -@leads_bp.route('/') -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def list_leads(): - leads = Lead.query.order_by(Lead.created_at.desc()).all() - return render_template('leads/list.html', leads=leads) - -@leads_bp.route('/new', methods=['GET', 'POST']) -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def new_lead(): - if request.method == 'POST': - if not has_write_permission('procurement'): - flash('Write access required to create leads.', 'danger') - return redirect(url_for('leads.list_leads')) - lead = Lead( - company_name=request.form['company_name'], - contact_name=request.form.get('contact_name'), - email=request.form.get('email'), - phone=request.form.get('phone'), - status=request.form.get('status', 'New'), - notes=request.form.get('notes') - ) - db.session.add(lead) - db.session.commit() - flash('Lead created successfully.', 'success') - return redirect(url_for('leads.list_leads')) - return render_template('leads/form.html') - -@leads_bp.route('//edit', methods=['GET', 'POST']) -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def edit_lead(id): - lead = db.get_or_404(Lead, id) - if request.method == 'POST': - if not has_write_permission('procurement'): - flash('Write access required to update leads.', 'danger') - return redirect(url_for('leads.list_leads')) - lead.company_name = request.form['company_name'] - lead.contact_name = request.form.get('contact_name') - lead.email = request.form.get('email') - lead.phone = request.form.get('phone') - lead.status = request.form.get('status') - lead.notes = request.form.get('notes') - db.session.commit() - flash('Lead updated successfully.', 'success') - return redirect(url_for('leads.list_leads')) - return render_template('leads/form.html', lead=lead) - -@leads_bp.route('//convert', methods=['GET', 'POST']) -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def convert_lead(id): - lead = db.get_or_404(Lead, id) - if lead.status == 'Converted': - flash('This lead has already been converted.', 'warning') - return redirect(url_for('leads.list_leads')) - - if request.method == 'POST': - if not has_write_permission('procurement'): - flash('Write access required to convert leads.', 'danger') - return redirect(url_for('leads.list_leads')) - conversion_type = request.form.get('conversion_type') - lead.status = 'Converted' - - if conversion_type == 'opportunity': - opportunity = Opportunity( - name=f"Opportunity from {lead.company_name}", - status='Evaluating' - ) - db.session.add(opportunity) - db.session.commit() - flash('Lead converted to a new Opportunity.', 'success') - return redirect(url_for('opportunities.edit_opportunity', id=opportunity.id)) - - elif conversion_type == 'supplier': - supplier = Supplier( - name=lead.company_name, - email=lead.email, - phone=lead.phone - ) - db.session.add(supplier) - db.session.commit() - flash('Lead converted to a new Supplier.', 'success') - return redirect(url_for('suppliers.edit_supplier', id=supplier.id)) - - return render_template('leads/convert.html', lead=lead) \ No newline at end of file diff --git a/src/routes/onboarding.py b/src/routes/onboarding.py index e78c5d0..a7979e1 100644 --- a/src/routes/onboarding.py +++ b/src/routes/onboarding.py @@ -19,6 +19,10 @@ onboarding_bp = Blueprint('onboarding', __name__) +_EP_INDEX = 'onboarding.index' +_EP_ONBOARDING = 'onboarding.onboarding_detail' +_EP_OFFBOARDING = 'onboarding.offboarding_detail' + # --- DASHBOARD PRINCIPAL --- @onboarding_bp.route('/') @@ -45,14 +49,14 @@ def index(): def new_pack(): if not has_write_permission('hr_people'): flash('Write access required to create packs.', 'danger') - return redirect(url_for('onboarding.index')) + return redirect(url_for(_EP_INDEX)) name = request.form.get('name') if name: pack = OnboardingPack(name=name, description=request.form.get('description')) db.session.add(pack) db.session.commit() flash(f'Pack "{name}" created.', 'success') - return redirect(url_for('onboarding.index')) + return redirect(url_for(_EP_INDEX)) @onboarding_bp.route('/packs') @login_required @@ -200,7 +204,7 @@ def new_onboarding(): if request.method == 'POST': if not has_write_permission('hr_people'): flash('Write access required to start onboarding.', 'danger') - return redirect(url_for('onboarding.index')) + return redirect(url_for(_EP_INDEX)) new_hire_name = request.form['new_hire_name'] target_email = request.form.get('target_email') personal_email = request.form.get('personal_email') @@ -297,7 +301,7 @@ def new_onboarding(): flash(f'{comm_count} emails scheduled based on pack communications.', 'info') flash(f'Onboarding started for {new_hire_name}.', 'success') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) packs = OnboardingPack.query.filter_by(is_active=True).all() users = User.query.filter_by(is_archived=False).all() # Para asignar usuario si ya existe @@ -318,21 +322,47 @@ def onboarding_detail(id): def update_process_details(id): if not has_write_permission('hr_people'): flash('Write access required to update process details.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=id)) + return redirect(url_for(_EP_ONBOARDING, id=id)) process = db.get_or_404(OnboardingProcess, id) process.target_email = request.form.get('target_email') process.personal_email = request.form.get('personal_email') - + process.notes = request.form.get('notes') or None + + start_date_str = request.form.get('start_date') + if start_date_str: + process.start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date() + manager_id = request.form.get('manager_id') buddy_id = request.form.get('buddy_id') - + process.assigned_manager_id = int(manager_id) if manager_id else None process.assigned_buddy_id = int(buddy_id) if buddy_id else None - + db.session.commit() flash('Process details updated.', 'success') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) + +@onboarding_bp.route('/offboarding//update_details', methods=['POST']) +@login_required +@requires_permission('hr_people') +def update_offboarding_details(id): + if not has_write_permission('hr_people'): + flash('Write access required to update process details.', 'danger') + return redirect(url_for(_EP_OFFBOARDING, id=id)) + process = db.get_or_404(OffboardingProcess, id) + + departure_date_str = request.form.get('departure_date') + if departure_date_str: + process.departure_date = datetime.strptime(departure_date_str, '%Y-%m-%d').date() + + manager_id = request.form.get('manager_id') + process.manager_id = int(manager_id) if manager_id else None + process.notes = request.form.get('notes') or None + + db.session.commit() + flash('Offboarding details updated.', 'success') + return redirect(url_for(_EP_OFFBOARDING, id=process.id)) # ========================================== # PROCESO DE OFFBOARDING (SALIDA) @@ -345,12 +375,15 @@ def new_offboarding(): if request.method == 'POST': if not has_write_permission('hr_people'): flash('Write access required to start offboarding.', 'danger') - return redirect(url_for('onboarding.index')) + return redirect(url_for(_EP_INDEX)) user_id = request.form['user_id'] departure_date = datetime.strptime(request.form['departure_date'], '%Y-%m-%d').date() target_user = db.get_or_404(User, user_id) - - manager_id = session.get('user_id') + + manager_id = session.get('user_id') + transfer_to_id = request.form.get('transfer_to_id') or None + if transfer_to_id: + transfer_to_id = int(transfer_to_id) process = OffboardingProcess( user_id=user_id, @@ -422,28 +455,34 @@ def new_offboarding(): linked_object_id=pm.id )) - # CORRECCIÓN RIESGOS: Usamos 'risk_description' que es el campo real en tu modelo + # RISKS risks = Risk.query.filter_by(owner_id=target_user.id).all() for r in risks: - # Acortamos la descripción si es muy larga para que quepa en el checklist short_desc = (r.risk_description[:75] + '..') if len(r.risk_description) > 75 else r.risk_description - db.session.add(ProcessItem( + item = ProcessItem( offboarding_process_id=process.id, description=f"⚠️ TRANSFER RISK: {short_desc}", item_type='Risk', linked_object_id=r.id - )) + ) + if transfer_to_id: + r.owner_id = transfer_to_id + item.is_completed = True + db.session.add(item) - # CORRECCIÓN SERVICIOS: Usamos 'BusinessService' - # 1. Transfer Ownership of Services owned by user + # SERVICE OWNERSHIP services_owned = BusinessService.query.filter_by(owner_id=target_user.id).all() for s in services_owned: - db.session.add(ProcessItem( + item = ProcessItem( offboarding_process_id=process.id, description=f"⚠️ TRANSFER SERVICE: {s.name} (User is Owner)", item_type='ServiceOwnership', linked_object_id=s.id - )) + ) + if transfer_to_id: + s.owner_id = transfer_to_id + item.is_completed = True + db.session.add(item) # 2. Revoke Access to Services/Applications # Find all services where the user is in the 'users' list @@ -457,17 +496,20 @@ def new_offboarding(): linked_object_id=s.id )) - # 3. CREDENTIALS - # Import Credential model + # CREDENTIALS from ..models.credentials import Credential credentials_owned = Credential.query.filter_by(owner_id=target_user.id, owner_type='User').all() for cred in credentials_owned: - db.session.add(ProcessItem( + item = ProcessItem( offboarding_process_id=process.id, description=f"🔑 REASSIGN CREDENTIAL: {cred.name} ({cred.type})", item_type='Credential', linked_object_id=cred.id - )) + ) + if transfer_to_id: + cred.owner_id = transfer_to_id + item.is_completed = True + db.session.add(item) # 4. TAREAS GLOBALES static_tasks = ProcessTemplate.query.filter_by(process_type='offboarding', is_active=True).all() @@ -478,9 +520,13 @@ def new_offboarding(): item_type='StaticTask' )) + transfer_to_user = db.session.get(User, transfer_to_id) if transfer_to_id else None db.session.commit() - flash(f'Offboarding started for {target_user.name}. Checklist created.', 'warning') - return redirect(url_for('onboarding.offboarding_detail', id=process.id)) + if transfer_to_user: + flash(f'Offboarding started for {target_user.name}. Ownership of risks, services and credentials auto-transferred to {transfer_to_user.name}.', 'warning') + else: + flash(f'Offboarding started for {target_user.name}. Checklist created.', 'warning') + return redirect(url_for(_EP_OFFBOARDING, id=process.id)) users = User.query.filter_by(is_archived=False).order_by(User.name).all() return render_template('onboarding/form_offboarding.html', users=users) @@ -538,11 +584,11 @@ def toggle_item(id): # Redirigir inteligentemente if item.onboarding_process_id: - return redirect(url_for('onboarding.onboarding_detail', id=item.onboarding_process_id)) + return redirect(url_for(_EP_ONBOARDING, id=item.onboarding_process_id)) elif item.offboarding_process_id: - return redirect(url_for('onboarding.offboarding_detail', id=item.offboarding_process_id)) + return redirect(url_for(_EP_OFFBOARDING, id=item.offboarding_process_id)) - return redirect(url_for('onboarding.index')) + return redirect(url_for(_EP_INDEX)) @onboarding_bp.route('/process///complete', methods=['POST']) @login_required @@ -569,7 +615,7 @@ def complete_process(type, id): flash(f'Offboarding completado. El usuario {process.user.name} ha sido archivado.', 'warning') db.session.commit() - return redirect(url_for('onboarding.index')) + return redirect(url_for(_EP_INDEX)) @onboarding_bp.route('/offboarding//revoke_service/', methods=['POST']) @login_required @@ -577,7 +623,7 @@ def complete_process(type, id): def revoke_service_access(process_id, item_id): if not has_write_permission('hr_people'): flash('Write access required to revoke access.', 'danger') - return redirect(url_for('onboarding.offboarding_detail', id=process_id)) + return redirect(url_for(_EP_OFFBOARDING, id=process_id)) process = db.get_or_404(OffboardingProcess, process_id) item = db.get_or_404(ProcessItem, item_id) @@ -588,30 +634,29 @@ def revoke_service_access(process_id, item_id): # So check offboarding_process_id if item.offboarding_process_id != process.id: flash('Invalid item for this process.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_OFFBOARDING, id=process.id)) if item.item_type != 'RevokeAccess': flash('Invalid item type.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_OFFBOARDING, id=process.id)) - service = db.session.get(BusinessService,item.linked_object_id) + service = db.session.get(BusinessService, item.linked_object_id) target_user = process.user - + if service and target_user: if target_user in service.users: service.users.remove(target_user) flash(f'User removed from {service.name}.', 'success') else: flash(f'User was not in {service.name} (already removed?).', 'warning') - - # Mark item as completed + item.is_completed = True item.completed_at = now() db.session.commit() else: flash('Service or User not found.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_OFFBOARDING, id=process.id)) @onboarding_bp.route('/offboarding//revoke_subscription/', methods=['POST']) @login_required @@ -619,17 +664,17 @@ def revoke_service_access(process_id, item_id): def revoke_subscription_access(process_id, item_id): if not has_write_permission('hr_people'): flash('Write access required to revoke access.', 'danger') - return redirect(url_for('onboarding.offboarding_detail', id=process_id)) + return redirect(url_for(_EP_OFFBOARDING, id=process_id)) process = db.get_or_404(OffboardingProcess, process_id) item = db.get_or_404(ProcessItem, item_id) if item.offboarding_process_id != process.id: flash('Invalid item for this process.', 'danger') - return redirect(url_for('onboarding.offboarding_detail', id=process_id)) + return redirect(url_for(_EP_OFFBOARDING, id=process_id)) if item.item_type != 'RevokeSubscriptionAccess': flash('Invalid item type.', 'danger') - return redirect(url_for('onboarding.offboarding_detail', id=process_id)) + return redirect(url_for(_EP_OFFBOARDING, id=process_id)) subscription = db.session.get(Subscription,item.linked_object_id) target_user = process.user @@ -651,7 +696,7 @@ def revoke_subscription_access(process_id, item_id): else: flash('Subscription or User not found.', 'danger') - return redirect(url_for('onboarding.offboarding_detail', id=process.id)) + return redirect(url_for(_EP_OFFBOARDING, id=process.id)) @onboarding_bp.route('/process//create_user/', methods=['POST']) @login_required @@ -659,23 +704,23 @@ def revoke_subscription_access(process_id, item_id): def create_user_account(process_id, item_id): if not has_write_permission('hr_people'): flash('Write access required to create user.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process_id)) + return redirect(url_for(_EP_ONBOARDING, id=process_id)) process = db.get_or_404(OnboardingProcess, process_id) item = db.get_or_404(ProcessItem, item_id) # Validation if item.onboarding_process_id != process.id: flash('Invalid item for this process.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process_id)) + return redirect(url_for(_EP_ONBOARDING, id=process_id)) if item.item_type != 'CreateUser': flash('Invalid item type.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process_id)) + return redirect(url_for(_EP_ONBOARDING, id=process_id)) # Check if user already linked if process.user_id: flash('Process already has a user linked.', 'warning') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) # Generate Email # Generate Email @@ -712,7 +757,7 @@ def create_user_account(process_id, item_id): db.session.commit() flash(f"User created!\nEmail: {email}\nPassword: {password}", "success") - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) @onboarding_bp.route('/process//add_to_service/', methods=['POST']) @login_required @@ -720,22 +765,22 @@ def create_user_account(process_id, item_id): def add_user_to_service(process_id, item_id): if not has_write_permission('hr_people'): flash('Write access required to add user to service.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process_id)) + return redirect(url_for(_EP_ONBOARDING, id=process_id)) process = db.get_or_404(OnboardingProcess, process_id) item = db.get_or_404(ProcessItem, item_id) if item.item_type != 'ServiceAccess' or not item.linked_object_id: flash('Invalid item type.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) service = db.session.get(BusinessService,item.linked_object_id) if not service: flash('Service not found.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) if not process.user: flash('No user linked to this onboarding process yet. Create the user first.', 'warning') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) if process.user not in service.users: service.users.append(process.user) @@ -749,7 +794,7 @@ def add_user_to_service(process_id, item_id): item.is_completed = True db.session.commit() - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) @onboarding_bp.route('/process//add_to_subscription/', methods=['POST']) @login_required @@ -757,22 +802,22 @@ def add_user_to_service(process_id, item_id): def add_user_to_subscription(process_id, item_id): if not has_write_permission('hr_people'): flash('Write access required to add user to subscription.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process_id)) + return redirect(url_for(_EP_ONBOARDING, id=process_id)) process = db.get_or_404(OnboardingProcess, process_id) item = db.get_or_404(ProcessItem, item_id) if item.item_type != 'Subscription' or not item.linked_object_id: flash('Invalid item type.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) subscription = db.session.get(Subscription,item.linked_object_id) if not subscription: flash('Subscription not found.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) if not process.user: flash('No user linked to this onboarding process yet. Create the user first.', 'warning') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) if process.user not in subscription.users: subscription.users.append(process.user) @@ -789,7 +834,7 @@ def add_user_to_subscription(process_id, item_id): item.is_completed = True db.session.commit() - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) @onboarding_bp.route('/process//add_to_course/', methods=['POST']) @login_required @@ -797,7 +842,7 @@ def add_user_to_subscription(process_id, item_id): def add_user_to_course(process_id, item_id): if not has_write_permission('hr_people'): flash('Write access required to add user to course.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process_id)) + return redirect(url_for(_EP_ONBOARDING, id=process_id)) from datetime import date, timedelta from ..models import CourseAssignment @@ -806,16 +851,16 @@ def add_user_to_course(process_id, item_id): if item.item_type != 'Course' or not item.linked_object_id: flash('Invalid item type.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) course = db.session.get(Course,item.linked_object_id) if not course: flash('Course not found.', 'danger') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) if not process.user: flash('No user linked to this onboarding process yet. Create the user first.', 'warning') - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) # Check if user is already assigned to this course existing = CourseAssignment.query.filter_by(course_id=course.id, user_id=process.user.id).first() @@ -831,7 +876,7 @@ def add_user_to_course(process_id, item_id): item.is_completed = True db.session.commit() - return redirect(url_for('onboarding.onboarding_detail', id=process.id)) + return redirect(url_for(_EP_ONBOARDING, id=process.id)) # ========================================== # API / UTILS @@ -1016,11 +1061,11 @@ def send_communication_now(id): if comm.status != 'pending': flash(f'Communication is already {comm.status}.', 'warning') - return redirect(request.referrer or url_for('onboarding.index')) + return redirect(request.referrer or url_for(_EP_INDEX)) if not comm.recipient_email: flash('No recipient email configured for this communication.', 'danger') - return redirect(request.referrer or url_for('onboarding.index')) + return redirect(request.referrer or url_for(_EP_INDEX)) try: # Get context and render template @@ -1051,7 +1096,7 @@ def send_communication_now(id): flash(f'Error sending email: {str(e)}', 'danger') db.session.commit() - return redirect(request.referrer or url_for('onboarding.index')) + return redirect(request.referrer or url_for(_EP_INDEX)) @onboarding_bp.route('/communications//cancel', methods=['POST']) @@ -1066,10 +1111,10 @@ def cancel_communication(id): if comm.status != 'pending': flash(f'Cannot cancel - communication is already {comm.status}.', 'warning') - return redirect(request.referrer or url_for('onboarding.index')) + return redirect(request.referrer or url_for(_EP_INDEX)) comm.status = 'cancelled' db.session.commit() flash(f'Communication "{comm.template.name}" cancelled.', 'info') - return redirect(request.referrer or url_for('onboarding.index')) \ No newline at end of file + return redirect(request.referrer or url_for(_EP_INDEX)) \ No newline at end of file diff --git a/src/routes/opportunities.py b/src/routes/opportunities.py deleted file mode 100644 index da62fb7..0000000 --- a/src/routes/opportunities.py +++ /dev/null @@ -1,159 +0,0 @@ -from flask import ( - Blueprint, render_template, request, redirect, url_for, flash -) -from datetime import datetime -from ..models import db, Opportunity, Activity, Supplier, Contact, Risk, Budget -from .main import login_required -from ..services.permissions_service import requires_permission, has_write_permission - -opportunities_bp = Blueprint('opportunities', __name__) - -@opportunities_bp.route('/') -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def list_opportunities(): - opportunities = Opportunity.query.order_by(Opportunity.estimated_close_date.asc()).all() - return render_template('opportunities/list.html', opportunities=opportunities) - -@opportunities_bp.route('/new', methods=['GET', 'POST']) -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def new_opportunity(): - if request.method == 'POST': - if not has_write_permission('procurement'): - flash('Write access required to create opportunities.', 'danger') - return redirect(url_for('opportunities.list_opportunities')) - opportunity = Opportunity( - name=request.form['name'], - status=request.form['status'], - potential_value=float(request.form.get('potential_value')) if request.form.get('potential_value') else None, - currency=request.form.get('currency'), - estimated_close_date=datetime.strptime(request.form['estimated_close_date'], '%Y-%m-%d').date() if request.form['estimated_close_date'] else None, - notes=request.form.get('notes'), - supplier_id=request.form.get('supplier_id') or None, - primary_contact_id=request.form.get('primary_contact_id') or None, - # Add risk_id and budget_id if you implemented previous changes - risk_id=request.form.get('risk_id') or None, - budget_id=request.form.get('budget_id') or None, - ) - db.session.add(opportunity) - db.session.commit() - flash('Opportunity created successfully!', 'success') - return redirect(url_for('opportunities.list_opportunities')) - - suppliers = Supplier.query.order_by(Supplier.name).all() - contacts = Contact.query.order_by(Contact.name).all() - # Add risks and budgets if you implemented previous changes - risks = Risk.query.order_by(Risk.risk_description).all() - budgets = Budget.query.order_by(Budget.name).all() - return render_template('opportunities/form.html', suppliers=suppliers, contacts=contacts, risks=risks, budgets=budgets) - -@opportunities_bp.route('/') -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def detail(id): - opportunity = db.get_or_404(Opportunity, id) - return render_template('opportunities/detail.html', opportunity=opportunity) - -# --- ADD THIS EDIT ROUTE --- -@opportunities_bp.route('//edit', methods=['GET', 'POST']) -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def edit_opportunity(id): - opportunity = db.get_or_404(Opportunity, id) - if request.method == 'POST': - if not has_write_permission('procurement'): - flash('Write access required to update opportunities.', 'danger') - return redirect(url_for('opportunities.detail', id=id)) - opportunity.name = request.form['name'] - opportunity.status = request.form['status'] - opportunity.potential_value = float(request.form.get('potential_value')) if request.form.get('potential_value') else None - opportunity.currency = request.form.get('currency') - opportunity.estimated_close_date = datetime.strptime(request.form['estimated_close_date'], '%Y-%m-%d').date() if request.form['estimated_close_date'] else None - opportunity.notes = request.form.get('notes') - opportunity.supplier_id = request.form.get('supplier_id') or None - opportunity.primary_contact_id = request.form.get('primary_contact_id') or None - # Add risk_id and budget_id updates if you implemented previous changes - opportunity.risk_id=request.form.get('risk_id') or None - opportunity.budget_id=request.form.get('budget_id') or None - db.session.commit() - flash('Opportunity updated successfully!', 'success') - return redirect(url_for('opportunities.detail', id=id)) # Redirect to detail after edit - - # Fetch necessary data for the form dropdowns - suppliers = Supplier.query.order_by(Supplier.name).all() - contacts = Contact.query.order_by(Contact.name).all() - # Add risks and budgets if you implemented previous changes - risks = Risk.query.order_by(Risk.risk_description).all() - budgets = Budget.query.order_by(Budget.name).all() - return render_template('opportunities/form.html', opportunity=opportunity, suppliers=suppliers, contacts=contacts, risks=risks, budgets=budgets) - - -@opportunities_bp.route('//add_activity', methods=['POST']) -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def add_activity(id): - if not has_write_permission('procurement'): - flash('Write access required to add activities.', 'danger') - return redirect(url_for('opportunities.detail', id=id)) - db.get_or_404(Opportunity, id) - activity_type = request.form.get('type') - notes = request.form.get('notes') - - if not notes: - flash('Activity notes cannot be empty.', 'danger') - else: - activity = Activity( - type=activity_type, - notes=notes, - opportunity_id=id - ) - db.session.add(activity) - db.session.commit() - flash('Activity added successfully.', 'success') - - return redirect(url_for('opportunities.detail', id=id)) - -@opportunities_bp.route('//add_task', methods=['POST']) -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def add_task(opportunity_id): - if not has_write_permission('procurement'): - flash('Write access required to add tasks.', 'danger') - return redirect(url_for('opportunities.detail', id=opportunity_id)) - description = request.form.get('task_description') - if description: - task = OpportunityTask(opportunity_id=opportunity_id, description=description) - db.session.add(task) - db.session.commit() - flash('Task added.', 'success') - else: - flash('Task description cannot be empty.', 'warning') - return redirect(url_for('opportunities.detail', id=opportunity_id)) - -@opportunities_bp.route('/task//toggle', methods=['POST']) -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def toggle_task(task_id): - if not has_write_permission('procurement'): - flash('Write access required to update tasks.', 'danger') - return redirect(url_for('opportunities.detail', id=db.get_or_404(OpportunityTask, task_id).opportunity_id)) - task = db.get_or_404(OpportunityTask, task_id) - task.is_completed = not task.is_completed - db.session.commit() - flash('Task status updated.', 'info') - return redirect(url_for('opportunities.detail', id=task.opportunity_id)) - -@opportunities_bp.route('/task//delete', methods=['POST']) -@login_required -@requires_permission('procurement', access_level='READ_ONLY') -def delete_task(task_id): - if not has_write_permission('procurement'): - flash('Write access required to delete tasks.', 'danger') - return redirect(url_for('opportunities.detail', id=db.get_or_404(OpportunityTask, task_id).opportunity_id)) - task = db.get_or_404(OpportunityTask, task_id) - opportunity_id = task.opportunity_id - db.session.delete(task) - db.session.commit() - flash('Task deleted.', 'success') - return redirect(url_for('opportunities.detail', id=opportunity_id)) \ No newline at end of file diff --git a/src/routes/peripherals.py b/src/routes/peripherals.py index 86192ec..8ed9583 100644 --- a/src/routes/peripherals.py +++ b/src/routes/peripherals.py @@ -126,6 +126,7 @@ def edit_peripheral(id): assets=Asset.query.order_by(Asset.name).all(), purchases=Purchase.query.order_by(Purchase.description).all(), suppliers=Supplier.query.order_by(Supplier.name).all(), + brands=Brand.query.order_by(Brand.name).all(), users=User.query.filter_by(is_archived=False).order_by(User.name).all()) @peripherals_bp.route('//checkout', methods=['GET', 'POST']) diff --git a/src/static/vendor/easymde/easymde.min.css b/src/static/vendor/easymde/easymde.min.css index 26f8790..8d29eae 100644 --- a/src/static/vendor/easymde/easymde.min.css +++ b/src/static/vendor/easymde/easymde.min.css @@ -1,5 +1,5 @@ /** - * easymde v2.20.0 + * easymde v2.21.0 * Copyright Jeroen Akkerman * @link https://github.com/ionaru/easy-markdown-editor * @license MIT diff --git a/src/templates/evaluations/detail.html b/src/templates/evaluations/detail.html index 0fbc205..43bed2e 100644 --- a/src/templates/evaluations/detail.html +++ b/src/templates/evaluations/detail.html @@ -1,6 +1,6 @@ {% extends "layout.html" %} -{% block title %}{{ evaluation.name }} - {{ super() %}}{%endblock %} +{% block title %}{{ evaluation.name }} - {{ super() }}{% endblock %} {% block content %}
diff --git a/src/templates/evaluations/form.html b/src/templates/evaluations/form.html index 46a9184..8b4990b 100644 --- a/src/templates/evaluations/form.html +++ b/src/templates/evaluations/form.html @@ -10,7 +10,7 @@

{{ title }}

-
+
diff --git a/src/templates/layout.html b/src/templates/layout.html index 1ae5e47..883cc03 100644 --- a/src/templates/layout.html +++ b/src/templates/layout.html @@ -179,13 +179,13 @@
- {% if pct == 100 and process.status != 'Completed' and can_write('hr_people') %} + {% if process.status != 'Completed' and can_write('hr_people') %}
+ class="mt-2" + {% if pct < 100 %}onsubmit="return confirm('{{ total - completed }} item(s) still pending. Close anyway?')"{% endif %}>
- {% if type == 'offboarding' %} -

Responsible Manager:
{{ process.manager.name if process.manager else 'N/A' }} -

- {% endif %} - {% if type == 'onboarding' %}
{% if can_write('hr_people') %} {% endif %}
+ + {% if type == 'offboarding' %} +

Responsible Manager
+ {{ process.manager.name if process.manager else 'N/A' }} +

+

Departure Date
+ {{ process.departure_date.strftime('%d %b, %Y') }} +

+ {% endif %} + + {% if type == 'onboarding' %}

Personal Email
{{ process.personal_email or 'Not set' }}

@@ -526,14 +533,21 @@
ℹ️ Details

Buddy
{{ process.assigned_buddy.name if process.assigned_buddy else 'Not assigned' }}

-
+

Start Date
+ {{ process.start_date.strftime('%d %b, %Y') }} +

{% endif %} - {% if process.personal_email and type != 'onboarding' %} -

Personal Email:
{{ - process.personal_email }}

+ {% if process.notes %} +
+

Notes
+ {{ process.notes }} +

{% endif %} -

Created on:
{{ process.created_at.strftime('%Y-%m-%d %H:%M') }}

+ +
+

Created on
+ {{ process.created_at.strftime('%Y-%m-%d %H:%M') }}

@@ -634,10 +648,15 @@
Email Timeline
+{% else %} + {% endif %} {% endblock %} \ No newline at end of file diff --git a/src/templates/opportunities/detail.html b/src/templates/opportunities/detail.html deleted file mode 100644 index 3faf5a2..0000000 --- a/src/templates/opportunities/detail.html +++ /dev/null @@ -1,82 +0,0 @@ -{% extends "layout.html" %} - -{% block title %}{{ opportunity.name }} - {{ super() }}{% endblock %} - -{% block content %} -
-

{{ opportunity.name }}

- {% if can_write('procurement') %} - - Edit - - {% endif %} -
- -
-
Evaluation Details
-
-
-
-

Status: {{ opportunity.status }}

-

Supplier: {{ opportunity.supplier.name if opportunity.supplier else 'N/A' }}

-

Primary Contact: {{ opportunity.primary_contact.name if opportunity.primary_contact - else 'N/A' }}

-
-
-

Potential Value: {{ opportunity.currency }} {{ - "%.2f"|format(opportunity.potential_value) if opportunity.potential_value else 'N/A' }}

-

Estimated Close Date: {{ opportunity.estimated_close_date.strftime('%Y-%m-%d') if - opportunity.estimated_close_date else 'N/A' }}

-
-
- {% if opportunity.notes %} -
-

Notes:

-

{{ opportunity.notes|nl2br }}

- {% endif %} -
-
- -
-
- Activity Log -
-
- {% if can_write('procurement') %} -
- -
- - -
-
-
- - -
-
- -
-
-
-
- {% endif %} -
    - {% for activity in opportunity.activities %} -
  • -

    {{ activity.type }} on {{ activity.activity_date.strftime('%Y-%m-%d @ - %H:%M') }}

    -

    {{ activity.notes|nl2br }}

    -
  • - {% else %} -
  • No evaluations logged yet.
  • - {% endfor %} -
-
-
-{% endblock %} \ No newline at end of file diff --git a/src/templates/opportunities/form.html b/src/templates/opportunities/form.html deleted file mode 100644 index 1885a62..0000000 --- a/src/templates/opportunities/form.html +++ /dev/null @@ -1,90 +0,0 @@ -{% extends "layout.html" %} - -{% set title = "New Evaluation" %} - -{% block title %}{{ title }} - {{ super() }}{% endblock %} - -{% block content %} -

{{ title }}

-
- -
-
-
- -
- - -
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
- Cancel - {% if can_write('procurement') %} - - {% endif %} -
-
-
-{% endblock %} \ No newline at end of file diff --git a/src/templates/opportunities/list.html b/src/templates/opportunities/list.html deleted file mode 100644 index ad53be4..0000000 --- a/src/templates/opportunities/list.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "layout.html" %} - -{% block title %}Evaluations - {{ super() }}{% endblock %} - -{% block content %} -
-

Evaluations

-
- - {% if can_write('procurement') %} - - New Evaluation - - {% endif %} -
-
- -
-
-
- - - - - - - - - - - - {% for op in opportunities %} - - - - - - - - {% else %} - - - - {% endfor %} - -
NameStatusSupplierPotential ValueEst. Close Date
{{ op.name }}{{ op.status }}{{ op.supplier.name if op.supplier else 'N/A' }}{{ op.currency }} {{ "%.2f"|format(op.potential_value) if op.potential_value else 'N/A' }} - {{ op.estimated_close_date.strftime('%Y-%m-%d') if op.estimated_close_date else 'N/A' }} -
No evaluations found.
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/tests/test_leads.py b/tests/test_leads.py deleted file mode 100644 index 83712a5..0000000 --- a/tests/test_leads.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest -from src.models import User -from src import db - -@pytest.fixture(scope='function') -def auth_user(app): - with app.app_context(): - # Check if user exists to avoid unique constraint error if DB isn't cleaned - user = User.query.filter_by(email='test_leads@example.com').first() - if not user: - user = User(name='Test User', email='test_leads@example.com', role='admin') - user.set_password('password') - db.session.add(user) - db.session.commit() - return user - -def test_render_new_lead_form(client, auth_user): - # Log in - client.post('/login', data={'email': 'test_leads@example.com', 'password': 'password'}) - - # This should trigger the TemplateSyntaxError - # We expect 500 or exception depending on how flask handles it in test mode. - # But for reproduction, we just want to see it fail or error out. - response = client.get('/leads/new') - assert response.status_code == 200 - assert b'New Requirement' in response.data