Compare commits
No commits in common. "453efb0db70b2886a5322a815db7cfb7f9f87e4c" and "9a6b8094dae44b3c1b861d671d54fb6495e85c9f" have entirely different histories.
453efb0db7
...
9a6b8094da
151 changed files with 1547 additions and 4980 deletions
13
.eslintignore
Normal file
13
.eslintignore
Normal file
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
44
.eslintrc.cjs
Normal file
44
.eslintrc.cjs
Normal file
|
@ -0,0 +1,44 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:svelte/recommended",
|
||||
],
|
||||
plugins: ["@typescript-eslint", "no-relative-import-paths"],
|
||||
ignorePatterns: ["*.cjs"],
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.svelte"],
|
||||
parser: "svelte-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
svelteFeatures: {
|
||||
experimentalGenerics: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {},
|
||||
globals: {
|
||||
$$Generic: "readonly",
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
"no-relative-import-paths/no-relative-import-paths": [
|
||||
"warn",
|
||||
{ allowSameFolder: true },
|
||||
],
|
||||
"no-console": "warn",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { args: "none" }],
|
||||
},
|
||||
};
|
|
@ -7,6 +7,11 @@ repos:
|
|||
pass_filenames: false
|
||||
entry: npm run check
|
||||
files: \.(js|ts|svelte)$
|
||||
- id: svelte-prettier
|
||||
name: prettier
|
||||
language: system
|
||||
entry: npx prettier --write --ignore-unknown
|
||||
types: [text]
|
||||
- id: svelte-lint
|
||||
name: eslint
|
||||
language: system
|
||||
|
|
14
.prettierignore
Normal file
14
.prettierignore
Normal file
|
@ -0,0 +1,14 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/run
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
9
.prettierrc
Normal file
9
.prettierrc
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 88,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
834
eslint.config.js
834
eslint.config.js
|
@ -1,834 +0,0 @@
|
|||
// @ts-expect-error js module
|
||||
import js from "@eslint/js";
|
||||
import stylistic from "@stylistic/eslint-plugin";
|
||||
// @ts-expect-error js module
|
||||
import eslintImport from "eslint-plugin-import";
|
||||
// @ts-expect-error js module
|
||||
import noRelativeImportPaths from "eslint-plugin-no-relative-import-paths";
|
||||
import svelte from "eslint-plugin-svelte";
|
||||
// @ts-expect-error js module
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import svelteParser from "svelte-eslint-parser";
|
||||
import ts from "typescript-eslint";
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs["flat/recommended"],
|
||||
// TS-Svelte
|
||||
{
|
||||
files: ["*.svelte", "**/*.svelte"],
|
||||
languageOptions: {
|
||||
parser: svelteParser,
|
||||
parserOptions: {
|
||||
parser: ts.parser,
|
||||
svelteFeatures: {
|
||||
experimentalGenerics: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// TS options
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2020,
|
||||
project: "./tsconfig.json",
|
||||
extraFileExtensions: [".svelte"],
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
$$Generic: "readonly",
|
||||
},
|
||||
},
|
||||
},
|
||||
// No relative import paths
|
||||
{
|
||||
plugins: {
|
||||
"no-relative-import-paths": noRelativeImportPaths,
|
||||
},
|
||||
rules: {
|
||||
"no-relative-import-paths/no-relative-import-paths": [
|
||||
"warn",
|
||||
{ allowSameFolder: true, rootDir: "src/lib", prefix: "$lib" },
|
||||
],
|
||||
},
|
||||
},
|
||||
// Import
|
||||
{
|
||||
plugins: {
|
||||
import: eslintImport,
|
||||
},
|
||||
rules: {
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
// Order: SvelteKit builtins, External deps, Internal deps, Components, Siblings
|
||||
groups: [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"unknown",
|
||||
["sibling", "parent", "index"],
|
||||
],
|
||||
"newlines-between": "always",
|
||||
alphabetize: {
|
||||
order: "asc",
|
||||
caseInsensitive: true,
|
||||
},
|
||||
pathGroups: [
|
||||
{ pattern: "$app/**", group: "builtin", position: "after" },
|
||||
{ pattern: "./$types", group: "builtin", position: "after" },
|
||||
{ pattern: "$lib/components/**", group: "unknown", position: "before" },
|
||||
{ pattern: "$lib/shared/**", group: "internal", position: "before" },
|
||||
],
|
||||
distinctGroup: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// No unused imports
|
||||
{
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "warn",
|
||||
"unused-imports/no-unused-vars": ["warn", { args: "none" }],
|
||||
},
|
||||
},
|
||||
// JS STYLING
|
||||
{
|
||||
plugins: {
|
||||
"@stylistic": stylistic,
|
||||
},
|
||||
rules: {
|
||||
// enforces line breaks after opening and before closing array brackets
|
||||
// https://eslint.style/rules/default/array-bracket-newline
|
||||
"@stylistic/array-bracket-newline": ["warn", "consistent"],
|
||||
|
||||
// enforce spacing inside array brackets
|
||||
// https://eslint.style/rules/default/array-bracket-spacing
|
||||
"@stylistic/array-bracket-spacing": ["warn", "never"],
|
||||
|
||||
// enforces line breaks between array elements
|
||||
// https://eslint.style/rules/default/array-element-newline
|
||||
"@stylistic/array-element-newline": ["warn", "consistent"],
|
||||
|
||||
// enforces parentheses in arrow functions
|
||||
// https://eslint.style/rules/default/arrow-parens
|
||||
"@stylistic/arrow-parens": "warn",
|
||||
|
||||
// enforces spacing before/after an arrow function's arrow
|
||||
// https://eslint.style/rules/default/arrow-spacing
|
||||
"@stylistic/arrow-spacing": "warn",
|
||||
|
||||
// enforce spacing inside single-line blocks
|
||||
// https://eslint.style/rules/default/block-spacing
|
||||
"@stylistic/block-spacing": ["warn", "always"],
|
||||
|
||||
// enforce one true brace style
|
||||
// https://eslint.style/rules/default/brace-style
|
||||
"@stylistic/brace-style": ["warn", "1tbs", { allowSingleLine: true }],
|
||||
|
||||
// require trailing commas in multiline object literals
|
||||
// https://eslint.style/rules/default/comma-dangle
|
||||
"@stylistic/comma-dangle": [
|
||||
"warn",
|
||||
{
|
||||
arrays: "always-multiline",
|
||||
objects: "always-multiline",
|
||||
imports: "always-multiline",
|
||||
exports: "always-multiline",
|
||||
functions: "always-multiline",
|
||||
enums: "always-multiline",
|
||||
generics: "always-multiline",
|
||||
tuples: "always-multiline",
|
||||
},
|
||||
],
|
||||
|
||||
// enforce spacing before and after comma
|
||||
// https://eslint.style/rules/default/comma-spacing
|
||||
"@stylistic/comma-spacing": ["warn", { before: false, after: true }],
|
||||
|
||||
// enforce one true comma style
|
||||
// https://eslint.style/rules/default/comma-style
|
||||
"@stylistic/comma-style": [
|
||||
"warn",
|
||||
"last",
|
||||
{
|
||||
exceptions: {
|
||||
ArrayExpression: false,
|
||||
ArrayPattern: false,
|
||||
ArrowFunctionExpression: false,
|
||||
CallExpression: false,
|
||||
FunctionDeclaration: false,
|
||||
FunctionExpression: false,
|
||||
ImportDeclaration: false,
|
||||
ObjectExpression: false,
|
||||
ObjectPattern: false,
|
||||
VariableDeclaration: false,
|
||||
NewExpression: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// disallow padding inside computed properties
|
||||
// https://eslint.style/rules/default/computed-property-spacing
|
||||
"@stylistic/computed-property-spacing": ["warn", "never"],
|
||||
|
||||
// enforce newlines before a dot in a member expression
|
||||
// https://eslint.style/rules/default/dot-location
|
||||
"@stylistic/dot-location": ["warn", "property"],
|
||||
|
||||
// enforce newline at the end of file, with no multiple empty lines
|
||||
// https://eslint.style/rules/default/eol-last
|
||||
"@stylistic/eol-last": ["warn", "always"],
|
||||
|
||||
// line breaks between arguments of a function call
|
||||
// https://eslint.style/rules/default/function-call-argument-newline
|
||||
"@stylistic/function-call-argument-newline": ["warn", "consistent"],
|
||||
|
||||
// enforce spacing between functions and their invocations
|
||||
// https://eslint.style/rules/default/function-call-spacing
|
||||
"@stylistic/function-call-spacing": ["warn", "never"],
|
||||
|
||||
// require function expressions to have a name
|
||||
// https://eslint.org/docs/rules/func-names
|
||||
"func-names": "warn",
|
||||
|
||||
// require line breaks inside function parentheses if there are line breaks between parameters
|
||||
// https://eslint.style/rules/default/function-paren-newline
|
||||
"@stylistic/function-paren-newline": ["warn", "multiline-arguments"],
|
||||
|
||||
// Enforce the location of arrow function bodies with implicit returns
|
||||
// https://eslint.style/rules/default/implicit-arrow-linebreak
|
||||
"@stylistic/implicit-arrow-linebreak": ["warn", "beside"],
|
||||
|
||||
// this option sets a specific tab width for your code
|
||||
// https://eslint.style/rules/default/indent
|
||||
"@stylistic/indent": [
|
||||
"warn",
|
||||
2,
|
||||
{
|
||||
SwitchCase: 1,
|
||||
VariableDeclarator: 1,
|
||||
outerIIFEBody: 1,
|
||||
// MemberExpression: null,
|
||||
FunctionDeclaration: {
|
||||
parameters: 1,
|
||||
body: 1,
|
||||
},
|
||||
FunctionExpression: {
|
||||
parameters: 1,
|
||||
body: 1,
|
||||
},
|
||||
CallExpression: {
|
||||
arguments: 1,
|
||||
},
|
||||
ArrayExpression: 1,
|
||||
ObjectExpression: 1,
|
||||
ImportDeclaration: 1,
|
||||
flatTernaryExpressions: false,
|
||||
// list derived from https://github.com/benjamn/ast-types/blob/HEAD/def/jsx.js
|
||||
ignoredNodes: [
|
||||
"JSXElement",
|
||||
"JSXElement > *",
|
||||
"JSXAttribute",
|
||||
"JSXIdentifier",
|
||||
"JSXNamespacedName",
|
||||
"JSXMemberExpression",
|
||||
"JSXSpreadAttribute",
|
||||
"JSXExpressionContainer",
|
||||
"JSXOpeningElement",
|
||||
"JSXClosingElement",
|
||||
"JSXFragment",
|
||||
"JSXOpeningFragment",
|
||||
"JSXClosingFragment",
|
||||
"JSXText",
|
||||
"JSXEmptyExpression",
|
||||
"JSXSpreadChild",
|
||||
],
|
||||
ignoreComments: false,
|
||||
},
|
||||
],
|
||||
|
||||
// enforces spacing between keys and values in object literal properties
|
||||
// https://eslint.style/rules/default/key-spacing
|
||||
"@stylistic/key-spacing": ["warn", { beforeColon: false, afterColon: true }],
|
||||
|
||||
// require a space before & after certain keywords
|
||||
// https://eslint.style/rules/default/keyword-spacing
|
||||
"@stylistic/keyword-spacing": [
|
||||
"warn",
|
||||
{
|
||||
before: true,
|
||||
after: true,
|
||||
overrides: {
|
||||
return: { after: true },
|
||||
throw: { after: true },
|
||||
case: { after: true },
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// disallow mixed 'LF' and 'CRLF' as linebreaks
|
||||
// https://eslint.style/rules/default/linebreak-style
|
||||
"@stylistic/linebreak-style": ["warn", "unix"],
|
||||
|
||||
// require or disallow an empty line between class members
|
||||
// https://eslint.style/rules/default/lines-between-class-members
|
||||
"@stylistic/lines-between-class-members": [
|
||||
"warn",
|
||||
"always",
|
||||
{ exceptAfterSingleLine: false },
|
||||
],
|
||||
|
||||
// require or disallow newlines around directives
|
||||
// https://eslint.org/docs/rules/lines-around-directive
|
||||
"lines-around-directive": [
|
||||
"warn",
|
||||
{
|
||||
before: "always",
|
||||
after: "always",
|
||||
},
|
||||
],
|
||||
|
||||
// specify the maximum length of a line in your program
|
||||
// https://eslint.style/rules/default/max-len
|
||||
"@stylistic/max-len": [
|
||||
"warn",
|
||||
100,
|
||||
2,
|
||||
{
|
||||
ignoreUrls: true,
|
||||
ignoreComments: false,
|
||||
ignoreRegExpLiterals: true,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
},
|
||||
],
|
||||
|
||||
// require a capital letter for constructors
|
||||
"new-cap": [
|
||||
"warn",
|
||||
{
|
||||
newIsCap: true,
|
||||
newIsCapExceptions: [],
|
||||
capIsNew: false,
|
||||
capIsNewExceptions: ["Immutable.Map", "Immutable.Set", "Immutable.List"],
|
||||
},
|
||||
],
|
||||
|
||||
// disallow the omission of parentheses when invoking a constructor with no arguments
|
||||
// https://eslint.style/rules/default/new-parens
|
||||
"@stylistic/new-parens": "warn",
|
||||
|
||||
// enforces new line after each method call in the chain to make it
|
||||
// more readable and easy to maintain
|
||||
// https://eslint.style/rules/default/newline-per-chained-call
|
||||
"@stylistic/newline-per-chained-call": ["warn", { ignoreChainWithDepth: 4 }],
|
||||
|
||||
// disallow use of the Array constructor
|
||||
"@typescript-eslint/no-array-constructor": "warn",
|
||||
|
||||
// disallow if as the only statement in an else block
|
||||
// https://eslint.org/docs/rules/no-lonely-if
|
||||
"no-lonely-if": "warn",
|
||||
|
||||
// disallow un-paren'd mixes of different operators
|
||||
// https://eslint.org/docs/rules/no-mixed-operators
|
||||
"@stylistic/no-mixed-operators": [
|
||||
"warn",
|
||||
{
|
||||
// the list of arithmetic groups disallows mixing `%` and `**`
|
||||
// with other arithmetic operators.
|
||||
groups: [
|
||||
["%", "**"],
|
||||
["%", "+"],
|
||||
["%", "-"],
|
||||
["%", "*"],
|
||||
["%", "/"],
|
||||
["/", "*"],
|
||||
["&", "|", "<<", ">>", ">>>"],
|
||||
["==", "!=", "===", "!=="],
|
||||
["&&", "||"],
|
||||
],
|
||||
allowSamePrecedence: false,
|
||||
},
|
||||
],
|
||||
|
||||
// disallow mixed spaces and tabs for indentation
|
||||
// https://eslint.style/rules/default/no-mixed-spaces-and-tabs
|
||||
"@stylistic/no-mixed-spaces-and-tabs": "warn",
|
||||
|
||||
// disallow use of chained assignment expressions
|
||||
// https://eslint.org/docs/rules/no-multi-assign
|
||||
"no-multi-assign": ["warn"],
|
||||
|
||||
// disallow multiple empty lines, only one newline at the end,
|
||||
// and no new lines at the beginning
|
||||
// https://eslint.style/rules/default/no-multiple-empty-lines
|
||||
"@stylistic/no-multiple-empty-lines": ["warn", { max: 1, maxBOF: 0, maxEOF: 0 }],
|
||||
|
||||
// disallow nested ternary expressions
|
||||
"no-nested-ternary": "warn",
|
||||
|
||||
// disallow use of the Object constructor
|
||||
"no-new-object": "warn",
|
||||
|
||||
// disallow certain syntax forms
|
||||
// https://eslint.org/docs/rules/no-restricted-syntax
|
||||
"no-restricted-syntax": [
|
||||
"warn",
|
||||
{
|
||||
selector: "ForInStatement",
|
||||
message:
|
||||
"for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.",
|
||||
},
|
||||
{
|
||||
selector: "LabeledStatement",
|
||||
message:
|
||||
"Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.",
|
||||
},
|
||||
{
|
||||
selector: "WithStatement",
|
||||
message:
|
||||
"`with` is disallowed in strict mode because it makes code impossible to predict and optimize.",
|
||||
},
|
||||
],
|
||||
|
||||
// disallow tab characters entirely
|
||||
// https://eslint.style/rules/default/no-tabs
|
||||
"@stylistic/no-tabs": "warn",
|
||||
|
||||
// disallow trailing whitespace at the end of lines
|
||||
// https://eslint.style/rules/default/no-trailing-spaces
|
||||
"svelte/no-trailing-spaces": [
|
||||
"warn",
|
||||
{
|
||||
skipBlankLines: false,
|
||||
ignoreComments: false,
|
||||
},
|
||||
],
|
||||
|
||||
// disallow dangling underscores in identifiers
|
||||
// https://eslint.org/docs/rules/no-underscore-dangle
|
||||
"no-underscore-dangle": [
|
||||
"warn",
|
||||
{
|
||||
allow: [],
|
||||
allowAfterThis: false,
|
||||
allowAfterSuper: false,
|
||||
enforceInMethodNames: true,
|
||||
},
|
||||
],
|
||||
|
||||
// disallow the use of Boolean literals in conditional expressions
|
||||
// also, prefer `a || b` over `a ? a : b`
|
||||
// https://eslint.org/docs/rules/no-unneeded-ternary
|
||||
"no-unneeded-ternary": ["warn", { defaultAssignment: false }],
|
||||
|
||||
// disallow whitespace before properties
|
||||
// https://eslint.style/rules/default/no-whitespace-before-property
|
||||
"@stylistic/no-whitespace-before-property": "warn",
|
||||
|
||||
// enforce the location of single-line statements
|
||||
// https://eslint.style/rules/default/nonblock-statement-body-position
|
||||
"@stylistic/nonblock-statement-body-position": ["warn", "beside", { overrides: {} }],
|
||||
|
||||
// require padding inside curly braces
|
||||
// https://eslint.style/rules/default/object-curly-spacing
|
||||
"@stylistic/object-curly-spacing": ["warn", "always"],
|
||||
|
||||
// enforce line breaks between braces
|
||||
// https://eslint.style/rules/default/object-curly-newline
|
||||
"@stylistic/object-curly-newline": [
|
||||
"warn",
|
||||
{
|
||||
ObjectExpression: { minProperties: 4, multiline: true, consistent: true },
|
||||
ObjectPattern: { minProperties: 4, multiline: true, consistent: true },
|
||||
ImportDeclaration: { minProperties: 4, multiline: true, consistent: true },
|
||||
ExportDeclaration: { minProperties: 4, multiline: true, consistent: true },
|
||||
},
|
||||
],
|
||||
|
||||
// enforce "same line" or "multiple line" on object properties.
|
||||
// https://eslint.style/rules/default/object-property-newline
|
||||
"@stylistic/object-property-newline": [
|
||||
"warn",
|
||||
{
|
||||
allowAllPropertiesOnSameLine: true,
|
||||
},
|
||||
],
|
||||
|
||||
// require assignment operator shorthand where possible or prohibit it entirely
|
||||
// https://eslint.org/docs/rules/operator-assignment
|
||||
"operator-assignment": ["warn", "always"],
|
||||
|
||||
// Requires operator at the beginning of the line in multiline statements
|
||||
// https://eslint.style/rules/default/operator-linebreak
|
||||
"@stylistic/operator-linebreak": ["warn", "before", { overrides: { "=": "none" } }],
|
||||
|
||||
// disallow padding within blocks
|
||||
// https://eslint.style/rules/default/padded-blocks
|
||||
"@stylistic/padded-blocks": [
|
||||
"warn",
|
||||
{
|
||||
blocks: "never",
|
||||
classes: "never",
|
||||
switches: "never",
|
||||
},
|
||||
{
|
||||
allowSingleLineBlocks: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Disallow the use of Math.pow in favor of the ** operator
|
||||
// https://eslint.org/docs/rules/prefer-exponentiation-operator
|
||||
"prefer-exponentiation-operator": "warn",
|
||||
|
||||
// Prefer use of an object spread over Object.assign
|
||||
// https://eslint.org/docs/rules/prefer-object-spread
|
||||
"prefer-object-spread": "warn",
|
||||
|
||||
// require quotes around object literal property names
|
||||
// https://eslint.style/rules/default/quote-props.html
|
||||
"@stylistic/quote-props": [
|
||||
"warn",
|
||||
"as-needed",
|
||||
{ keywords: false, unnecessary: true, numbers: false },
|
||||
],
|
||||
|
||||
// specify whether double or single quotes should be used
|
||||
// https://eslint.style/rules/default/quotes
|
||||
"@stylistic/quotes": ["warn", "double", { avoidEscape: true }],
|
||||
|
||||
// require or disallow use of semicolons instead of ASI
|
||||
// https://eslint.style/rules/default/semi
|
||||
"@stylistic/semi": ["warn", "always"],
|
||||
|
||||
// enforce spacing before and after semicolons
|
||||
// https://eslint.style/rules/default/semi-spacing
|
||||
"@stylistic/semi-spacing": ["warn", { before: false, after: true }],
|
||||
|
||||
// Enforce location of semicolons
|
||||
// https://eslint.style/rules/default/semi-style
|
||||
"@stylistic/semi-style": ["warn", "last"],
|
||||
|
||||
// require or disallow space before blocks
|
||||
// https://eslint.style/rules/default/space-before-blocks
|
||||
"@stylistic/space-before-blocks": "warn",
|
||||
|
||||
// require or disallow space before function opening parenthesis
|
||||
// https://eslint.style/rules/default/space-before-function-paren
|
||||
"@stylistic/space-before-function-paren": [
|
||||
"warn",
|
||||
{
|
||||
anonymous: "always",
|
||||
named: "never",
|
||||
asyncArrow: "always",
|
||||
},
|
||||
],
|
||||
|
||||
// require or disallow spaces inside parentheses
|
||||
// https://eslint.style/rules/default/space-in-parens
|
||||
"@stylistic/space-in-parens": ["warn", "never"],
|
||||
|
||||
// require spaces around operators
|
||||
// https://eslint.style/rules/default/space-infix-ops
|
||||
"@stylistic/space-infix-ops": "warn",
|
||||
|
||||
// Require or disallow spaces before/after unary operators
|
||||
// https://eslint.style/rules/default/space-unary-ops
|
||||
"@stylistic/space-unary-ops": [
|
||||
"warn",
|
||||
{
|
||||
words: true,
|
||||
nonwords: false,
|
||||
overrides: {},
|
||||
},
|
||||
],
|
||||
|
||||
// require or disallow a space immediately following the // or /* in a comment
|
||||
// https://eslint.style/rules/default/spaced-comment
|
||||
"@stylistic/spaced-comment": [
|
||||
"warn",
|
||||
"always",
|
||||
{
|
||||
line: {
|
||||
exceptions: ["-", "+"],
|
||||
markers: ["=", "!", "/"], // space here to support sprockets directives, slash for TS /// comments
|
||||
},
|
||||
block: {
|
||||
exceptions: ["-", "+"],
|
||||
markers: ["=", "!", ":", "::"], // space here to support sprockets directives and flow comment types
|
||||
balanced: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Enforce spacing around colons of switch statements
|
||||
// https://eslint.style/rules/default/switch-colon-spacing
|
||||
"@stylistic/switch-colon-spacing": ["warn", { after: true, before: false }],
|
||||
|
||||
// Require or disallow spacing between template tags and their literals
|
||||
// https://eslint.style/rules/default/template-tag-spacing
|
||||
"@stylistic/template-tag-spacing": ["warn", "never"],
|
||||
|
||||
// Disallow multiple spaces
|
||||
// https://eslint.style/rules/default/no-multi-spaces
|
||||
"@stylistic/no-multi-spaces": "warn",
|
||||
},
|
||||
},
|
||||
// BEST PRACTICES
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/default-param-last": "warn",
|
||||
"@typescript-eslint/dot-notation": ["warn", { allowKeywords: true }],
|
||||
"@typescript-eslint/no-implied-eval": "error",
|
||||
"@typescript-eslint/no-loop-func": "warn",
|
||||
"@typescript-eslint/no-redeclare": "error",
|
||||
"@typescript-eslint/no-unused-expressions": [
|
||||
"warn",
|
||||
{
|
||||
allowShortCircuit: false,
|
||||
allowTernary: false,
|
||||
allowTaggedTemplates: false,
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/return-await": "error",
|
||||
|
||||
"@typescript-eslint/no-shadow": ["error", { allow: ["i", "j"] }],
|
||||
"no-shadow-restricted-names": "error",
|
||||
"@typescript-eslint/no-loss-of-precision": "error",
|
||||
},
|
||||
},
|
||||
|
||||
// ERRORS
|
||||
{
|
||||
rules: {
|
||||
// Enforce “for” loop update clause moving the counter in the right direction
|
||||
// https://eslint.org/docs/rules/for-direction
|
||||
"for-direction": "error",
|
||||
|
||||
// Enforces that a return statement is present in property getters
|
||||
// https://eslint.org/docs/rules/getter-return
|
||||
"getter-return": ["error", { allowImplicit: true }],
|
||||
|
||||
// disallow using an async function as a Promise executor
|
||||
// https://eslint.org/docs/rules/no-async-promise-executor
|
||||
"no-async-promise-executor": "error",
|
||||
|
||||
// Disallow await inside of loops
|
||||
// https://eslint.org/docs/rules/no-await-in-loop
|
||||
"no-await-in-loop": "error",
|
||||
|
||||
// Disallow comparisons to negative zero
|
||||
// https://eslint.org/docs/rules/no-compare-neg-zero
|
||||
"no-compare-neg-zero": "error",
|
||||
|
||||
// disallow assignment in conditional expressions
|
||||
"no-cond-assign": ["error", "always"],
|
||||
|
||||
// disallow use of console
|
||||
"no-console": "warn",
|
||||
|
||||
// Disallows expressions where the operation doesn't affect the value
|
||||
// https://eslint.org/docs/rules/no-constant-binary-expression
|
||||
"no-constant-binary-expression": "error",
|
||||
|
||||
// disallow use of constant expressions in conditions
|
||||
"no-constant-condition": "warn",
|
||||
|
||||
// disallow control characters in regular expressions
|
||||
"no-control-regex": "error",
|
||||
|
||||
// disallow use of debugger
|
||||
"no-debugger": "error",
|
||||
|
||||
// disallow duplicate arguments in functions
|
||||
"no-dupe-args": "error",
|
||||
|
||||
// Disallow duplicate conditions in if-else-if chains
|
||||
// https://eslint.org/docs/rules/no-dupe-else-if
|
||||
"no-dupe-else-if": "error",
|
||||
|
||||
// disallow duplicate keys when creating object literals
|
||||
"no-dupe-keys": "error",
|
||||
|
||||
// disallow a duplicate case label.
|
||||
"no-duplicate-case": "error",
|
||||
|
||||
// disallow empty statements
|
||||
"no-empty": "error",
|
||||
|
||||
// disallow the use of empty character classes in regular expressions
|
||||
"no-empty-character-class": "error",
|
||||
|
||||
// disallow assigning to the exception in a catch block
|
||||
"no-ex-assign": "error",
|
||||
|
||||
// disallow double-negation boolean casts in a boolean context
|
||||
// https://eslint.org/docs/rules/no-extra-boolean-cast
|
||||
"no-extra-boolean-cast": "error",
|
||||
|
||||
// disallow unnecessary parentheses
|
||||
// https://eslint.org/docs/rules/no-extra-parens
|
||||
"@typescript-eslint/no-extra-parens": [
|
||||
"warn",
|
||||
"all",
|
||||
{
|
||||
conditionalAssign: true,
|
||||
nestedBinaryExpressions: false,
|
||||
returnAssign: false,
|
||||
ignoreJSX: "all", // delegate to eslint-plugin-react
|
||||
enforceForArrowConditionals: false,
|
||||
},
|
||||
],
|
||||
|
||||
// disallow unnecessary semicolons
|
||||
"no-extra-semi": "error",
|
||||
|
||||
// disallow overwriting functions written as function declarations
|
||||
"no-func-assign": "error",
|
||||
|
||||
// https://eslint.org/docs/rules/no-import-assign
|
||||
"no-import-assign": "error",
|
||||
|
||||
// disallow function or variable declarations in nested blocks
|
||||
"svelte/no-inner-declarations": "error",
|
||||
|
||||
// disallow invalid regular expression strings in the RegExp constructor
|
||||
"no-invalid-regexp": "error",
|
||||
|
||||
// disallow irregular whitespace outside of strings and comments
|
||||
"no-irregular-whitespace": "error",
|
||||
|
||||
// Disallow Number Literals That Lose Precision
|
||||
// https://eslint.org/docs/rules/no-loss-of-precision
|
||||
"no-loss-of-precision": "error",
|
||||
|
||||
// Disallow characters which are made with multiple code points in character class syntax
|
||||
// https://eslint.org/docs/rules/no-misleading-character-class
|
||||
"no-misleading-character-class": "error",
|
||||
|
||||
// disallow the use of object properties of the global object (Math and JSON) as functions
|
||||
"no-obj-calls": "error",
|
||||
|
||||
// Disallow new operators with global non-constructor functions
|
||||
// https://eslint.org/docs/latest/rules/no-new-native-nonconstructor
|
||||
"no-new-native-nonconstructor": "error",
|
||||
|
||||
// Disallow returning values from Promise executor functions
|
||||
// https://eslint.org/docs/rules/no-promise-executor-return
|
||||
"no-promise-executor-return": "error",
|
||||
|
||||
// disallow use of Object.prototypes builtins directly
|
||||
// https://eslint.org/docs/rules/no-prototype-builtins
|
||||
"no-prototype-builtins": "error",
|
||||
|
||||
// disallow multiple spaces in a regular expression literal
|
||||
"no-regex-spaces": "error",
|
||||
|
||||
// Disallow returning values from setters
|
||||
// https://eslint.org/docs/rules/no-setter-return
|
||||
"no-setter-return": "error",
|
||||
|
||||
// disallow sparse arrays
|
||||
"no-sparse-arrays": "error",
|
||||
|
||||
// Disallow template literal placeholder syntax in regular strings
|
||||
// https://eslint.org/docs/rules/no-template-curly-in-string
|
||||
"no-template-curly-in-string": "error",
|
||||
|
||||
// Avoid code that looks like two expressions but is actually one
|
||||
// https://eslint.org/docs/rules/no-unexpected-multiline
|
||||
"no-unexpected-multiline": "error",
|
||||
|
||||
// disallow unreachable statements after a return, throw, continue, or break statement
|
||||
"no-unreachable": "error",
|
||||
|
||||
// Disallow loops with a body that allows only one iteration
|
||||
// https://eslint.org/docs/rules/no-unreachable-loop
|
||||
"no-unreachable-loop": "error",
|
||||
|
||||
// disallow return/throw/break/continue inside finally blocks
|
||||
// https://eslint.org/docs/rules/no-unsafe-finally
|
||||
"no-unsafe-finally": "error",
|
||||
|
||||
// disallow negating the left operand of relational operators
|
||||
// https://eslint.org/docs/rules/no-unsafe-negation
|
||||
"no-unsafe-negation": "error",
|
||||
|
||||
// disallow use of optional chaining in contexts where the undefined value is not allowed
|
||||
// https://eslint.org/docs/rules/no-unsafe-optional-chaining
|
||||
"no-unsafe-optional-chaining": ["error", { disallowArithmeticOperators: true }],
|
||||
|
||||
// Disallow Unused Private Class Members
|
||||
// https://eslint.org/docs/rules/no-unused-private-class-members
|
||||
"no-unused-private-class-members": "warn",
|
||||
|
||||
// Disallow useless backreferences in regular expressions
|
||||
// https://eslint.org/docs/rules/no-useless-backreference
|
||||
"no-useless-backreference": "error",
|
||||
|
||||
// disallow comparisons with the value NaN
|
||||
"use-isnan": "error",
|
||||
|
||||
// ensure that the results of typeof are compared against a valid string
|
||||
// https://eslint.org/docs/rules/valid-typeof
|
||||
"valid-typeof": ["error", { requireStringLiterals: true }],
|
||||
},
|
||||
},
|
||||
|
||||
// Svelte errors
|
||||
{
|
||||
rules: {
|
||||
"svelte/block-lang": ["error", { script: "ts", style: "postcss" }],
|
||||
"svelte/no-target-blank": "error",
|
||||
"svelte/valid-each-key": "error",
|
||||
"svelte/require-optimized-style-attribute": "warn",
|
||||
},
|
||||
},
|
||||
|
||||
// Svelte styling
|
||||
{
|
||||
rules: {
|
||||
"svelte/first-attribute-linebreak": "warn",
|
||||
"svelte/html-closing-bracket-spacing": "warn",
|
||||
"svelte/html-quotes": "warn",
|
||||
"svelte/html-self-closing": "warn",
|
||||
"svelte/indent": "warn",
|
||||
"svelte/max-attributes-per-line": [
|
||||
"warn",
|
||||
{
|
||||
multiline: 1,
|
||||
singleline: 4,
|
||||
},
|
||||
],
|
||||
"svelte/mustache-spacing": "warn",
|
||||
"svelte/no-extra-reactive-curlies": "warn",
|
||||
"svelte/no-spaces-around-equal-signs-in-attribute": "warn",
|
||||
"svelte/prefer-class-directive": "warn",
|
||||
"svelte/prefer-style-directive": "warn",
|
||||
"svelte/shorthand-attribute": "warn",
|
||||
"svelte/shorthand-directive": "warn",
|
||||
"svelte/sort-attributes": "warn",
|
||||
},
|
||||
},
|
||||
|
||||
// Ignore
|
||||
{
|
||||
ignores: [
|
||||
"build/",
|
||||
".svelte-kit/",
|
||||
"*.config.cjs",
|
||||
"vite.config.ts.timestamp-*",
|
||||
".tmp/",
|
||||
],
|
||||
},
|
||||
];
|
56
package.json
56
package.json
|
@ -7,24 +7,23 @@
|
|||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest --run && vitest --config vitest.config.integration.js --run",
|
||||
"test": "vitest --run && vitest --config vitest.config.integration.ts --run",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "eslint .",
|
||||
"format": "eslint . --fix",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test:unit": "vitest",
|
||||
"test:integration": "vitest --config vitest.config.integration.js",
|
||||
"test:integration": "vitest --config vitest.config.integration.ts",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.30.0",
|
||||
"@auth/core": "^0.28.2",
|
||||
"@floating-ui/core": "^1.6.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@prisma/client": "^5.12.1",
|
||||
"carta-md": "^4.0.0",
|
||||
"diff": "^5.2.0",
|
||||
"isomorphic-dompurify": "^2.7.0",
|
||||
"qs": "^6.12.1",
|
||||
"isomorphic-dompurify": "^2.6.0",
|
||||
"marked": "^12.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"zod": "^3.22.4",
|
||||
|
@ -32,47 +31,40 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@stylistic/eslint-plugin": "^1.7.2",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@sveltejs/adapter-node": "^5.0.1",
|
||||
"@sveltejs/kit": "^2.5.6",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||
"@sveltejs/kit": "^2.5.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@trpc/client": "^10.45.2",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@types/diff": "^5.0.9",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/qs": "^6.9.15",
|
||||
"@types/node": "^20.12.4",
|
||||
"@types/set-cookie-parser": "^2.4.7",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.10.2",
|
||||
"daisyui": "^4.9.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-no-relative-import-paths": "^1.5.4",
|
||||
"eslint-plugin-svelte": "^2.37.0",
|
||||
"eslint-plugin-unused-imports": "^3.1.0",
|
||||
"globals": "^15.0.0",
|
||||
"eslint-plugin-no-relative-import-paths": "^1.5.3",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-nesting": "^12.1.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.2",
|
||||
"prisma": "^5.12.1",
|
||||
"svelte": "^4.2.15",
|
||||
"svelte": "^4.2.12",
|
||||
"svelte-check": "^3.6.9",
|
||||
"sveltekit-superforms": "^2.12.5",
|
||||
"sveltekit-superforms": "^2.12.2",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"trpc-sveltekit": "^3.6.1",
|
||||
"tslib": "^2.6.2",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.7.0",
|
||||
"vite": "^5.2.9",
|
||||
"vitest": "^1.5.0"
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.8",
|
||||
"vitest": "^1.4.0"
|
||||
},
|
||||
"type": "module",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"carta-md@4.0.0": "patches/carta-md@4.0.0.patch"
|
||||
}
|
||||
}
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
# Change "markdown-body" css class to "prose" for Tailwind compatibility
|
||||
diff --git a/dist/Markdown.svelte b/dist/Markdown.svelte
|
||||
index 92b29fade303a14539720b9bc389e7a41202b1cf..cbdeede16d7af17a481bcac519aea92b8959803d 100644
|
||||
--- a/dist/Markdown.svelte
|
||||
+++ b/dist/Markdown.svelte
|
||||
@@ -15,7 +15,7 @@ onMount(async () => {
|
||||
});
|
||||
</script>
|
||||
|
||||
-<div bind:this={elem} class="carta-viewer carta-theme__{theme} markdown-body">
|
||||
+<div bind:this={elem} class="carta-viewer carta-theme__{theme} prose">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html rendered}
|
||||
{#if mounted}
|
||||
diff --git a/dist/internal/components/Renderer.svelte b/dist/internal/components/Renderer.svelte
|
||||
index 1d2ff1a6937bc490ea1e6eb5c2ef9f3b33e4c326..6a95c154ea4ca7c4d19b20d02b3504fb2b65b7f7 100644
|
||||
--- a/dist/internal/components/Renderer.svelte
|
||||
+++ b/dist/internal/components/Renderer.svelte
|
||||
@@ -17,7 +17,7 @@ onMount(() => carta.$setRenderer(elem));
|
||||
onMount(() => mounted = true);
|
||||
</script>
|
||||
|
||||
-<div bind:this={elem} on:scroll={handleScroll} class="carta-renderer markdown-body">
|
||||
+<div bind:this={elem} on:scroll={handleScroll} class="carta-renderer prose">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html renderedHtml}
|
||||
{#if mounted}
|
|
@ -1,10 +1,12 @@
|
|||
import { defineConfig } from "@playwright/test";
|
||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: "npm run build && npm run preview",
|
||||
port: 4173,
|
||||
},
|
||||
testDir: "tests",
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
|
||||
});
|
||||
};
|
||||
|
||||
export default config;
|
2326
pnpm-lock.yaml
2326
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,4 @@
|
|||
import generateMockdata from "../tests/helpers/generate-mockdata";
|
||||
// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths
|
||||
import generateMockdata from "../tests/helpers/generate-mockdata"
|
||||
|
||||
await generateMockdata();
|
||||
|
|
36
src/app.d.ts
vendored
36
src/app.d.ts
vendored
|
@ -20,40 +20,4 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
declare module "@gramex/url/encode" {
|
||||
export default function encode(obj: unknown, settings?: {
|
||||
listBracket?: boolean;
|
||||
listIndex?: boolean;
|
||||
objBracket?: boolean;
|
||||
sortKeys?: boolean;
|
||||
drop?: unknown[];
|
||||
}): string;
|
||||
|
||||
}
|
||||
declare module "@gramex/url/index" {
|
||||
export { default as encode } from "@gramex/url/encode";
|
||||
import { default as update } from "@gramex/url/update";
|
||||
export { update };
|
||||
export const decode: (url: string, settings?: {
|
||||
convert?: boolean;
|
||||
forceList?: boolean;
|
||||
pruneString?: boolean;
|
||||
}) => unknown;
|
||||
|
||||
}
|
||||
declare module "@gramex/url/update" {
|
||||
export default function update(obj: unknown, url: string, settings?: {
|
||||
convert?: boolean;
|
||||
forceList?: boolean;
|
||||
pruneString?: boolean;
|
||||
pruneObject?: boolean;
|
||||
pruneArray?: boolean;
|
||||
}): unknown;
|
||||
|
||||
}
|
||||
declare module "@gramex/url" {
|
||||
import main = require("@gramex/url/index");
|
||||
export = main;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
22
src/app.pcss
22
src/app.pcss
|
@ -3,20 +3,6 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.btn-transition {
|
||||
@apply duration-200 ease-out;
|
||||
animation: button-pop var(--animation-btn, 0.25s) ease-out;
|
||||
transition-property: color, background-color, border-color, opacity, box-shadow,
|
||||
transform;
|
||||
&:active:hover,
|
||||
&:active:focus {
|
||||
animation: button-pop 0s ease-out;
|
||||
transform: scale(var(--btn-focus-scale, 0.97));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
text-align: initial;
|
||||
}
|
||||
|
@ -31,6 +17,10 @@ button {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.border-1 {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.v-form-field > input {
|
||||
@apply input input-bordered w-full max-w-xs;
|
||||
}
|
||||
|
@ -44,7 +34,7 @@ button {
|
|||
@apply bg-base-200;
|
||||
@apply rounded-xl;
|
||||
@apply flex flex-col;
|
||||
@apply border border-solid border-base-content/30;
|
||||
@apply border-solid border-base-content/30 border-[1px];
|
||||
|
||||
.row {
|
||||
@apply flex flex-row;
|
||||
|
@ -53,7 +43,7 @@ button {
|
|||
.row,
|
||||
.rowb {
|
||||
@apply p-2;
|
||||
@apply border-t border-solid border-base-content/30;
|
||||
@apply border-solid border-base-content/30 border-t-[1px];
|
||||
}
|
||||
|
||||
.row:first-child {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { HandleClientError } from "@sveltejs/kit";
|
||||
import { TRPCClientError } from "@trpc/client";
|
||||
|
||||
const CHECK_CONNECTION = "Die Seite konnte nicht geladen werden, prüfen sie ihre Verbindung";
|
||||
const CHECK_CONNECTION =
|
||||
"Die Seite konnte nicht geladen werden, prüfen sie ihre Verbindung";
|
||||
|
||||
export const handleError: HandleClientError = async ({ error, message, status }) => {
|
||||
// If there are client-side errors, SvelteKit always returns the nondescript
|
||||
|
@ -12,8 +13,8 @@ export const handleError: HandleClientError = async ({ error, message, status })
|
|||
console.error("Client error:", error);
|
||||
|
||||
if (
|
||||
error instanceof TypeError
|
||||
&& error.message.includes("dynamically imported module")
|
||||
error instanceof TypeError &&
|
||||
error.message.includes("dynamically imported module")
|
||||
) {
|
||||
// Could not load JS module
|
||||
message = CHECK_CONNECTION;
|
||||
|
|
|
@ -2,9 +2,9 @@ import { redirect, type Handle } from "@sveltejs/kit";
|
|||
import { sequence } from "@sveltejs/kit/hooks";
|
||||
import { createTRPCHandle } from "trpc-sveltekit";
|
||||
|
||||
import { skAuthHandle } from "$lib/server/auth";
|
||||
import { createContext } from "$lib/server/trpc/context";
|
||||
import { router } from "$lib/server/trpc/router";
|
||||
import { skAuthHandle } from "$lib/server/auth";
|
||||
|
||||
/**
|
||||
* Protect the application against unauthorized access.
|
||||
|
@ -18,7 +18,7 @@ const authorization: Handle = async ({ event, resolve }) => {
|
|||
if (!/^\/(login|trpc)/.test(event.url.pathname)) {
|
||||
if (!event.locals.session) {
|
||||
const params = new URLSearchParams({ returnURL: event.url.pathname });
|
||||
redirect(303, `/login?${params.toString()}`);
|
||||
redirect(303, "/login?" + params.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,5 +29,5 @@ const authorization: Handle = async ({ event, resolve }) => {
|
|||
export const handle = sequence(
|
||||
skAuthHandle,
|
||||
authorization,
|
||||
createTRPCHandle({ router, createContext }),
|
||||
createTRPCHandle({ router, createContext })
|
||||
);
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
import { formatDate, humanDate } from "$lib/shared/util";
|
||||
|
||||
import PatientCard from "$lib/components/entry/PatientCard.svelte";
|
||||
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
||||
import UserField from "$lib/components/table/UserField.svelte";
|
||||
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
||||
import Markdown from "$lib/components/ui/Markdown.svelte";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import Markdown from "$lib/components/ui/markdown/Markdown.svelte";
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import VersionsButton from "$lib/components/ui/VersionsButton.svelte";
|
||||
import PatientCard from "$lib/components/entry/PatientCard.svelte";
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
|
||||
export let entry: RouterOutput["entry"]["get"];
|
||||
export let withExecution = false;
|
||||
|
@ -24,14 +21,14 @@
|
|||
<title>Eintrag #{entry.id}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Header backHref={backBtn ? basePath : undefined} title="Eintrag #{entry.id}">
|
||||
<Header title="Eintrag #{entry.id}" backHref={backBtn ? basePath : undefined}>
|
||||
{#if entry.current_version.category}
|
||||
<CategoryField category={entry.current_version.category} />
|
||||
{/if}
|
||||
{#if entry.current_version.priority}
|
||||
<div class="badge ellipsis badge-warning">Priorität</div>
|
||||
{/if}
|
||||
<a slot="rightBtn" class="btn btn-sm btn-primary ml-auto" href="{basePath}/edit">
|
||||
<a slot="rightBtn" href="{basePath}/edit" class="btn btn-sm btn-primary ml-auto">
|
||||
Bearbeiten
|
||||
</a>
|
||||
</Header>
|
||||
|
@ -53,17 +50,19 @@
|
|||
Beschreibung
|
||||
<div>
|
||||
<VersionsButton href="{basePath}/versions" n={entry.n_versions} />
|
||||
<a class="btn btn-circle btn-sm btn-ghost" href="{basePath}/edit">
|
||||
<a href="{basePath}/edit" class="btn btn-circle btn-sm btn-ghost">
|
||||
<Icon path={mdiPencil} size={1.2} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p class="prose">
|
||||
<Markdown src={entry.current_version.text} />
|
||||
</p>
|
||||
</div>
|
||||
<div class="rowb c-vlight text-sm">
|
||||
Zuletzt bearbeitet am {formatDate(entry.current_version.created_at, true)} von
|
||||
<UserField filterName="author" user={entry.current_version.author} />
|
||||
<UserField user={entry.current_version.author} filterName="author" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -72,18 +71,20 @@
|
|||
<div class="row c-light text-sm items-center justify-between">
|
||||
<p>
|
||||
Erledigt am {formatDate(entry.execution.created_at, true)} von
|
||||
<UserField filterName="executor" user={entry.execution.author} />
|
||||
<UserField user={entry.execution.author} filterName="executor" />
|
||||
</p>
|
||||
<div>
|
||||
<VersionsButton href="{basePath}/executions" n={entry.n_executions} />
|
||||
<a class="btn btn-circle btn-xs btn-ghost" href="{basePath}/editExecution">
|
||||
<a href="{basePath}/editExecution" class="btn btn-circle btn-xs btn-ghost">
|
||||
<Icon path={mdiPencil} size={1.2} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{#if entry.execution?.text}
|
||||
<div class="row">
|
||||
<p class="prose">
|
||||
<Markdown src={entry.execution?.text} />
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
|
||||
import RoomField from "$lib/components/table/RoomField.svelte";
|
||||
|
||||
export let patient: RouterOutput["patient"]["list"]["items"][0];
|
||||
|
|
|
@ -7,15 +7,12 @@
|
|||
*/
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import { createFloatingActions } from "svelte-floating-ui";
|
||||
import { shift } from "svelte-floating-ui/dom";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import outclick from "$lib/actions/outclick";
|
||||
|
||||
import type { BaseItem } from "./types";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type T = $$Generic<BaseItem>;
|
||||
type OnSelectResult = { newValue: string; close: boolean };
|
||||
|
@ -90,9 +87,9 @@
|
|||
return false;
|
||||
} else {
|
||||
isLoading = true;
|
||||
items().then((fetchedItems) => {
|
||||
srcItems = fetchedItems;
|
||||
if (cacheKey) cache[cacheKey] = fetchedItems;
|
||||
items().then((items) => {
|
||||
srcItems = items;
|
||||
if (cacheKey) cache[cacheKey] = items;
|
||||
isLoading = false;
|
||||
updateSearch();
|
||||
});
|
||||
|
@ -137,12 +134,14 @@
|
|||
|
||||
function updateSearch() {
|
||||
if (loadSrcItems()) {
|
||||
const searchWord = inputValue().toLowerCase().trim();
|
||||
filteredItems = !selection && searchWord.length > 0
|
||||
let searchWord = inputValue().toLowerCase().trim();
|
||||
filteredItems =
|
||||
!selection && searchWord.length > 0
|
||||
? srcItems.filter(
|
||||
(it) => !hiddenIds.has(it.id)
|
||||
&& filterFn(it)
|
||||
&& it.name.toLowerCase().includes(searchWord),
|
||||
(it) =>
|
||||
!hiddenIds.has(it.id) &&
|
||||
filterFn(it) &&
|
||||
it.name.toLowerCase().includes(searchWord)
|
||||
)
|
||||
: srcItems.filter((it) => !hiddenIds.has(it.id) && filterFn(it));
|
||||
markSelection();
|
||||
|
@ -180,7 +179,7 @@
|
|||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
let { key } = e;
|
||||
let key = e.key;
|
||||
if (key === "Tab" && e.shiftKey) key = "ShiftTab";
|
||||
const fnmap: { [key: string]: () => void } = {
|
||||
Tab: () => close,
|
||||
|
@ -278,14 +277,13 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-grow {cls}" on:outclick={close} use:outclick>
|
||||
<div class="flex-grow {cls}" use:outclick on:outclick={close}>
|
||||
<input
|
||||
bind:this={inputElm}
|
||||
class={inputCls}
|
||||
class:px-2={padding}
|
||||
{placeholder}
|
||||
type="text"
|
||||
value=""
|
||||
{placeholder}
|
||||
bind:this={inputElm}
|
||||
on:input={onInput}
|
||||
on:click={open}
|
||||
on:focus={open}
|
||||
|
@ -293,31 +291,30 @@
|
|||
on:keypress={onKeyPress}
|
||||
on:blur={onBlur}
|
||||
use:floatingRef
|
||||
value=""
|
||||
/>
|
||||
|
||||
{#if opened && filteredItems.length > 0}
|
||||
<div bind:this={listElm} class="autocomplete-list" use:floatingContent>
|
||||
<div class="autocomplete-list" use:floatingContent bind:this={listElm}>
|
||||
{#each filteredItems as item, i}
|
||||
<div
|
||||
role="option"
|
||||
class="autocomplete-list-item"
|
||||
class:selected={i === highlightIndex}
|
||||
aria-selected={i === highlightIndex}
|
||||
role="option"
|
||||
tabindex="-1"
|
||||
on:click|preventDefault={() => {
|
||||
selectListItem(item, false);
|
||||
}}
|
||||
on:keypress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
selectListItem(item, true);
|
||||
}
|
||||
e.key == "Enter" && selectListItem(item, true);
|
||||
}}
|
||||
on:pointerenter={() => {
|
||||
highlightIndex = i;
|
||||
}}
|
||||
>
|
||||
{#if item.icon}
|
||||
<Icon path={item.icon} size={1.2} />
|
||||
<Icon size={1.2} path={item.icon} />
|
||||
{/if}
|
||||
{item.name}
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { mdiClose } from "@mdi/js";
|
||||
|
||||
import { Debouncer } from "$lib/shared/util";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
|
||||
import Autocomplete from "./Autocomplete.svelte";
|
||||
import EntryFilterChip from "./FilterChip.svelte";
|
||||
import Autocomplete from "./Autocomplete.svelte";
|
||||
import type {
|
||||
FilterDef,
|
||||
FilterQdata,
|
||||
|
@ -16,11 +10,13 @@
|
|||
SelectionOrText,
|
||||
} from "./types";
|
||||
import { isFilterValueless } from "./types";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import { Debouncer } from "$lib/shared/util";
|
||||
|
||||
/** Filter definitions */
|
||||
export let FILTERS: { [key: string]: FilterDef };
|
||||
/** Filter data from the query */
|
||||
export let filterData: FilterQdata | null | undefined;
|
||||
export let filterData: FilterQdata | null | undefined = undefined;
|
||||
/** Callback when filters are updated */
|
||||
export let onUpdate: (filterData: FilterQdata | undefined) => void = () => {};
|
||||
/** List of hidden filter IDs, can be specified for prefiltered views (e.g. by patient) */
|
||||
|
@ -30,9 +26,9 @@
|
|||
|
||||
let autocomplete: Autocomplete<BaseItem> | undefined;
|
||||
let activeFilters: FilterData[] = [];
|
||||
const cache: { [key: string]: BaseItem[] } = {};
|
||||
let cache: { [key: string]: BaseItem[] } = {};
|
||||
let searchVal = "";
|
||||
const searchDebounce = new Debouncer(400, () => {
|
||||
let searchDebounce = new Debouncer(400, () => {
|
||||
onUpdate(getFilterQdata());
|
||||
});
|
||||
|
||||
|
@ -42,9 +38,7 @@
|
|||
if (f.toggleOff) {
|
||||
return [
|
||||
f,
|
||||
{
|
||||
id: f.id, name: f.toggleOff.name, icon: f.toggleOff.icon, toggle: false,
|
||||
},
|
||||
{ id: f.id, name: f.toggleOff.name, icon: f.toggleOff.icon, toggle: false },
|
||||
];
|
||||
}
|
||||
return [f];
|
||||
|
@ -52,12 +46,9 @@
|
|||
|
||||
// Filter menu items to be hidden
|
||||
$: hiddenIds = new Set([
|
||||
...Object.values(FILTERS).flatMap((f) => {
|
||||
return f.inputType === 2
|
||||
|| activeFilters.every((af) => af.id !== f.id)
|
||||
? []
|
||||
: [f.id];
|
||||
}),
|
||||
...Object.values(FILTERS).flatMap((f) =>
|
||||
f.inputType === 2 || activeFilters.every((af) => af.id !== f.id) ? [] : [f.id]
|
||||
),
|
||||
...hiddenFilters,
|
||||
]);
|
||||
|
||||
|
@ -66,9 +57,9 @@
|
|||
updateFromQueryData(filterData ?? {});
|
||||
}
|
||||
|
||||
function updateFromQueryData(fd: FilterQdata) {
|
||||
function updateFromQueryData(filterData: FilterQdata) {
|
||||
const filters: FilterData[] = [];
|
||||
for (const [id, value] of Object.entries(fd)) {
|
||||
for (const [id, value] of Object.entries(filterData)) {
|
||||
// If filter is hidden or undefined, dont display it
|
||||
if (hiddenFilters.includes(id) || !FILTERS[id]) continue;
|
||||
// Extract search parameter if a separate search field is used
|
||||
|
@ -83,7 +74,7 @@
|
|||
});
|
||||
});
|
||||
} else {
|
||||
const selection: SelectionOrText = {};
|
||||
let selection: SelectionOrText = {};
|
||||
if (typeof value === "string") selection.name = value;
|
||||
else if (typeof value === "boolean") selection.toggle = value;
|
||||
else selection.id = value;
|
||||
|
@ -97,31 +88,36 @@
|
|||
function getHiddenIds(fid: string, fpos: number): Set<string | number> {
|
||||
if (FILTERS[fid].inputType === 2) {
|
||||
return new Set(
|
||||
activeFilters.flatMap((f, i) => (i !== fpos && f.selection?.id ? [f.selection?.id] : [])),
|
||||
activeFilters.flatMap((f, i) => {
|
||||
return i !== fpos && f.selection?.id ? [f.selection?.id] : [];
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
if (autocomplete) autocomplete.open();
|
||||
}
|
||||
|
||||
function getFilterQdata(): FilterQdata | undefined {
|
||||
const fd: FilterQdata = {};
|
||||
let fd: FilterQdata = {};
|
||||
activeFilters.forEach((fdata) => {
|
||||
const filter = FILTERS[fdata.id];
|
||||
const key = filter.id;
|
||||
|
||||
let val = null;
|
||||
if (filter.inputType === 0) {
|
||||
// Valueless filter (val = true)
|
||||
if (filter.inputType === 0) {
|
||||
val = true;
|
||||
} else if (filter.inputType === 1) {
|
||||
}
|
||||
// Text input
|
||||
else if (filter.inputType === 1) {
|
||||
val = fdata.selection?.name;
|
||||
} else if (filter.inputType === 2 && fdata.selection) {
|
||||
}
|
||||
// Filter list
|
||||
else if (filter.inputType === 2 && fdata.selection) {
|
||||
//@ts-expect-error TODO
|
||||
val = { id: fdata.selection.id, name: fdata.selection.name };
|
||||
} else if (filter.inputType === 3) {
|
||||
|
@ -141,7 +137,7 @@
|
|||
});
|
||||
|
||||
if (searchVal) {
|
||||
fd.search = searchVal;
|
||||
fd["search"] = searchVal;
|
||||
}
|
||||
|
||||
if (Object.keys(fd).length === 0) return undefined;
|
||||
|
@ -167,8 +163,9 @@
|
|||
}
|
||||
|
||||
function removeFilter(i: number) {
|
||||
const shouldUpdate = isFilterValueless(FILTERS[activeFilters[i].id].inputType)
|
||||
|| activeFilters[i].selection !== null;
|
||||
const shouldUpdate =
|
||||
isFilterValueless(FILTERS[activeFilters[i].id].inputType) ||
|
||||
activeFilters[i].selection !== null;
|
||||
activeFilters.splice(i, 1);
|
||||
activeFilters = activeFilters;
|
||||
if (shouldUpdate) updateFilter();
|
||||
|
@ -193,10 +190,10 @@
|
|||
<div class="filterbar-inner input input-sm input-bordered">
|
||||
{#each activeFilters as fdata, i}
|
||||
<EntryFilterChip
|
||||
{cache}
|
||||
{fdata}
|
||||
filter={FILTERS[fdata.id]}
|
||||
{fdata}
|
||||
hiddenIds={() => getHiddenIds(fdata.id, i)}
|
||||
{cache}
|
||||
onRemove={() => removeFilter(i)}
|
||||
onSelection={(sel, kb) => {
|
||||
updateFilter();
|
||||
|
@ -207,19 +204,19 @@
|
|||
<Autocomplete
|
||||
bind:this={autocomplete}
|
||||
cls="mr-8"
|
||||
{hiddenIds}
|
||||
items={filterMenuItems}
|
||||
{hiddenIds}
|
||||
placeholder="Filter"
|
||||
onSelect={(item) => {
|
||||
const close = addFilter(item);
|
||||
return { newValue: "", close };
|
||||
}}
|
||||
onBackspace={() => {
|
||||
activeFilters.pop();
|
||||
activeFilters = activeFilters;
|
||||
updateFilter();
|
||||
}}
|
||||
onSelect={(item) => {
|
||||
const close = addFilter(item);
|
||||
return { newValue: "", close };
|
||||
}}
|
||||
partOfFilterbar
|
||||
placeholder="Filter"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-ghost absolute bottom-0 right-0"
|
||||
|
@ -230,14 +227,14 @@
|
|||
updateFilter();
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiClose} size={1.2} />
|
||||
<Icon size={1.2} path={mdiClose} />
|
||||
</button>
|
||||
</div>
|
||||
{#if search}
|
||||
<input
|
||||
class="input input-sm input-bordered"
|
||||
placeholder="Suche"
|
||||
type="text"
|
||||
placeholder="Suche"
|
||||
bind:value={searchVal}
|
||||
on:input={onSearchInput}
|
||||
on:keypress={onSearchKeypress}
|
||||
|
@ -253,9 +250,6 @@
|
|||
|
||||
.filterbar-inner {
|
||||
@apply flex flex-wrap flex-grow items-stretch h-min p-0 gap-2 relative;
|
||||
|
||||
:global(input) {
|
||||
height: 32px;
|
||||
}
|
||||
line-height: 30px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { mdiClose } from "@mdi/js";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
|
||||
import type { BaseItem, FilterData, FilterDef, SelectionOrText } from "./types";
|
||||
import Autocomplete from "./Autocomplete.svelte";
|
||||
import type {
|
||||
BaseItem, FilterData, FilterDef, SelectionOrText,
|
||||
} from "./types";
|
||||
|
||||
export let filter: FilterDef;
|
||||
export let hiddenIds: () => Set<string | number> = () => new Set();
|
||||
|
@ -63,7 +59,7 @@
|
|||
}}
|
||||
>
|
||||
{#if filterIcon}
|
||||
<Icon path={filterIcon} size={1.2} />
|
||||
<Icon size={1.2} path={filterIcon} />
|
||||
{/if}
|
||||
<span class="flex items-center">
|
||||
{filterName + (hasInputField ? ":" : "")}
|
||||
|
@ -76,32 +72,33 @@
|
|||
{@const hids = hiddenIds()}
|
||||
<Autocomplete
|
||||
bind:this={autocomplete}
|
||||
items={filter.options ?? []}
|
||||
hiddenIds={hids}
|
||||
{cache}
|
||||
cacheKey={filter.id}
|
||||
hiddenIds={hids}
|
||||
items={filter.options ?? []}
|
||||
onBackspace={onRemove}
|
||||
{onClose}
|
||||
selection={fdata.selection?.id
|
||||
? { id: fdata.selection?.id, name: fdata.selection?.name ?? "" }
|
||||
: null}
|
||||
padding={false}
|
||||
onSelect={(item) => {
|
||||
// Accept the selection if this is a free text field or the user selected a variant
|
||||
if (filter.inputType !== 2 || item.id) {
|
||||
fdata.selection = item;
|
||||
return { close: true, newValue: "" };
|
||||
}
|
||||
} else {
|
||||
return { close: false, newValue: item.name ?? "" };
|
||||
}
|
||||
}}
|
||||
padding={false}
|
||||
{onClose}
|
||||
onBackspace={onRemove}
|
||||
partOfFilterbar
|
||||
selection={fdata.selection?.id
|
||||
? { id: fdata.selection?.id, name: fdata.selection?.name ?? "" }
|
||||
: null}
|
||||
/>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
class="bg-transparent"
|
||||
autofocus
|
||||
type="text"
|
||||
autofocus
|
||||
value={fdata.selection?.name ?? ""}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === "Escape") onClose(true);
|
||||
|
@ -132,6 +129,6 @@
|
|||
aria-label={`Filter "${filterName}" entfernen"`}
|
||||
on:click={onRemove}
|
||||
>
|
||||
<Icon path={mdiClose} size={1} />
|
||||
<Icon size={1} path={mdiClose} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { trpc } from "$lib/shared/trpc";
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiAccountInjury,
|
||||
|
@ -12,9 +13,6 @@ import {
|
|||
mdiMagnify,
|
||||
mdiTag,
|
||||
} from "@mdi/js";
|
||||
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
|
||||
import type { FilterDef } from "./types";
|
||||
|
||||
export const ENTRY_FILTERS: { [key: string]: FilterDef } = {
|
||||
|
@ -23,42 +21,54 @@ export const ENTRY_FILTERS: { [key: string]: FilterDef } = {
|
|||
name: "Kategorie",
|
||||
icon: mdiTag,
|
||||
inputType: 2,
|
||||
options: async () => trpc().category.list.query(),
|
||||
options: async () => {
|
||||
return await trpc().category.list.query();
|
||||
},
|
||||
},
|
||||
author: {
|
||||
id: "author",
|
||||
name: "Autor",
|
||||
icon: mdiAccount,
|
||||
inputType: 2,
|
||||
options: async () => trpc().user.getNames.query(),
|
||||
options: async () => {
|
||||
return await trpc().user.getNames.query();
|
||||
},
|
||||
},
|
||||
executor: {
|
||||
id: "executor",
|
||||
name: "Erledigt von",
|
||||
icon: mdiDoctor,
|
||||
inputType: 2,
|
||||
options: async () => trpc().user.getNames.query(),
|
||||
options: async () => {
|
||||
return await trpc().user.getNames.query();
|
||||
},
|
||||
},
|
||||
patient: {
|
||||
id: "patient",
|
||||
name: "Patient",
|
||||
icon: mdiAccountInjury,
|
||||
inputType: 2,
|
||||
options: async () => trpc().patient.getNames.query(),
|
||||
options: async () => {
|
||||
return await trpc().patient.getNames.query();
|
||||
},
|
||||
},
|
||||
station: {
|
||||
id: "station",
|
||||
name: "Station",
|
||||
icon: mdiDomain,
|
||||
inputType: 2,
|
||||
options: async () => trpc().station.list.query(),
|
||||
options: async () => {
|
||||
return await trpc().station.list.query();
|
||||
},
|
||||
},
|
||||
room: {
|
||||
id: "room",
|
||||
name: "Zimmer",
|
||||
icon: mdiBedKingOutline,
|
||||
inputType: 2,
|
||||
options: async () => trpc().room.list.query(),
|
||||
options: async () => {
|
||||
return await trpc().room.list.query();
|
||||
},
|
||||
},
|
||||
done: {
|
||||
id: "done",
|
||||
|
@ -90,14 +100,18 @@ export const PATIENT_FILTER: { [key: string]: FilterDef } = {
|
|||
name: "Station",
|
||||
icon: mdiDomain,
|
||||
inputType: 2,
|
||||
options: async () => trpc().station.list.query(),
|
||||
options: async () => {
|
||||
return await trpc().station.list.query();
|
||||
},
|
||||
},
|
||||
room: {
|
||||
id: "room",
|
||||
name: "Zimmer",
|
||||
icon: mdiBedKingOutline,
|
||||
inputType: 2,
|
||||
options: async () => trpc().room.list.query(),
|
||||
options: async () => {
|
||||
return await trpc().room.list.query();
|
||||
},
|
||||
},
|
||||
hidden: {
|
||||
id: "hidden",
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import type { Category } from "$lib/shared/model";
|
||||
import { ZCategoryNew } from "$lib/shared/model/validation";
|
||||
|
||||
import FormField from "$lib/components/ui/FormField.svelte";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
|
||||
const schema = zod(ZCategoryNew);
|
||||
|
||||
export let category: Category | null = null;
|
||||
export let formData = defaults(schema);
|
||||
|
||||
const {
|
||||
form, errors, constraints, enhance, tainted,
|
||||
} = superForm(formData, {
|
||||
validators: schema,
|
||||
resetForm: category === null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<Header backHref="/categories" title={category ? `Kategorie #${category.id}` : "Neue Kategorie"} />
|
||||
|
||||
<form class="flex flex-col gap-4" method="POST" use:enhance>
|
||||
<FormField errors={$errors.name} label="Name">
|
||||
<input
|
||||
name="name"
|
||||
aria-invalid={Boolean($errors.name)}
|
||||
type="text"
|
||||
bind:value={$form.name}
|
||||
{...$constraints.name}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField errors={$errors.color} label="Farbe">
|
||||
<input
|
||||
name="color"
|
||||
aria-invalid={Boolean($errors.color)}
|
||||
type="color"
|
||||
bind:value={$form.color}
|
||||
{...$constraints.color}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField errors={$errors.description} label="Beschreibung">
|
||||
<input
|
||||
name="description"
|
||||
aria-invalid={Boolean($errors.description)}
|
||||
type="text"
|
||||
bind:value={$form.description}
|
||||
{...$constraints.description}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="btn btn-primary max-w-32"
|
||||
disabled={browser && category && $tainted === undefined}
|
||||
type="submit"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
|
@ -1,15 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import { ZPatientNew } from "$lib/shared/model/validation";
|
||||
import FormField from "$lib/components/ui/FormField.svelte";
|
||||
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
|
||||
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
||||
import FormField from "$lib/components/ui/FormField.svelte";
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
import { ZPatientNew } from "$lib/shared/model/validation";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
const schema = zod(ZPatientNew);
|
||||
|
||||
|
@ -18,11 +15,8 @@
|
|||
|
||||
let station = patient?.room?.station;
|
||||
|
||||
const {
|
||||
form, errors, constraints, enhance, tainted,
|
||||
} = superForm(formData, {
|
||||
const { form, errors, constraints, enhance, tainted } = superForm(formData, {
|
||||
validators: schema,
|
||||
resetForm: patient === null,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -34,31 +28,31 @@
|
|||
{/if}
|
||||
</h1>
|
||||
|
||||
<form class="flex flex-col gap-4" method="POST" use:enhance>
|
||||
<form method="POST" class="flex flex-col gap-4" use:enhance>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<FormField errors={$errors.first_name} label="Vorname">
|
||||
<FormField label="Vorname" errors={$errors.first_name}>
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
aria-invalid={Boolean($errors.first_name)}
|
||||
type="text"
|
||||
bind:value={$form.first_name}
|
||||
{...$constraints.last_name}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField errors={$errors.last_name} label="Nachname">
|
||||
<FormField label="Nachname" errors={$errors.last_name}>
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
aria-invalid={Boolean($errors.last_name)}
|
||||
type="text"
|
||||
bind:value={$form.last_name}
|
||||
{...$constraints.last_name}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField errors={$errors.age} label="Alter">
|
||||
<FormField label="Alter" errors={$errors.age}>
|
||||
<input
|
||||
type="number"
|
||||
name="age"
|
||||
aria-invalid={Boolean($errors.age)}
|
||||
type="number"
|
||||
bind:value={$form.age}
|
||||
{...$constraints.age}
|
||||
/>
|
||||
|
@ -66,11 +60,13 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<FormField errors={$errors.room_id} label="Zimmer">
|
||||
<FormField label="Zimmer" errors={$errors.room_id}>
|
||||
<Autocomplete
|
||||
idInputName="room_id"
|
||||
inputCls="input input-bordered w-full max-w-xs"
|
||||
items={async () => trpc().room.list.query()}
|
||||
items={async () => {
|
||||
return await trpc().room.list.query();
|
||||
}}
|
||||
selection={patient?.room}
|
||||
onSelect={(item) => {
|
||||
station = item.station;
|
||||
$form.room_id = item.id;
|
||||
|
@ -78,15 +74,15 @@
|
|||
onUnselect={() => {
|
||||
$form.room_id = null;
|
||||
}}
|
||||
selection={patient?.room}
|
||||
idInputName="room_id"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Station">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
disabled
|
||||
type="text"
|
||||
value={station?.name ?? ""}
|
||||
/>
|
||||
</FormField>
|
||||
|
@ -95,8 +91,8 @@
|
|||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="btn btn-primary max-w-32"
|
||||
disabled={browser && patient && $tainted === undefined}
|
||||
type="submit"
|
||||
disabled={browser && $tainted === undefined}
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import { ZRoomNew } from "$lib/shared/model/validation";
|
||||
import { trpc, type RouterOutput } from "$lib/shared/trpc";
|
||||
|
||||
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
||||
import FormField from "$lib/components/ui/FormField.svelte";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
|
||||
const schema = zod(ZRoomNew);
|
||||
|
||||
export let room: RouterOutput["room"]["get"] | null = null;
|
||||
export let formData = defaults(schema);
|
||||
|
||||
const {
|
||||
form, errors, constraints, enhance, tainted,
|
||||
} = superForm(formData, {
|
||||
validators: schema,
|
||||
resetForm: room === null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<Header backHref="/rooms" title={room ? `Zimmer #${room.id}` : "Neues Zimmer"} />
|
||||
|
||||
<form class="flex flex-col gap-4" method="POST" use:enhance>
|
||||
<FormField errors={$errors.name} label="Name">
|
||||
<input
|
||||
name="name"
|
||||
aria-invalid={Boolean($errors.name)}
|
||||
type="text"
|
||||
bind:value={$form.name}
|
||||
{...$constraints.name}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField errors={$errors.station_id} label="Station">
|
||||
<Autocomplete
|
||||
idInputName="station_id"
|
||||
inputCls="input input-bordered w-full max-w-xs"
|
||||
items={async () => trpc().station.list.query()}
|
||||
onSelect={(item) => {
|
||||
$form.station_id = item.id;
|
||||
}}
|
||||
onUnselect={() => {
|
||||
$form.station_id = 0;
|
||||
}}
|
||||
selection={room?.station}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="btn btn-primary max-w-32"
|
||||
disabled={browser && room && $tainted === undefined}
|
||||
type="submit"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
|
@ -1,50 +0,0 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import type { Station } from "$lib/shared/model";
|
||||
import { ZStationNew } from "$lib/shared/model/validation";
|
||||
|
||||
import FormField from "$lib/components/ui/FormField.svelte";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
|
||||
const schema = zod(ZStationNew);
|
||||
|
||||
export let station: Station | null = null;
|
||||
export let formData = defaults(schema);
|
||||
|
||||
const {
|
||||
form, errors, constraints, enhance, tainted,
|
||||
} = superForm(formData, {
|
||||
validators: schema,
|
||||
resetForm: station === null,
|
||||
});
|
||||
</script>
|
||||
|
||||
<Header backHref="/stations" title={station ? `Station #${station.id}` : "Neue Station"} />
|
||||
|
||||
<form class="flex flex-col gap-4" method="POST" use:enhance>
|
||||
<FormField errors={$errors.name} label="Name">
|
||||
<input
|
||||
name="name"
|
||||
aria-invalid={Boolean($errors.name)}
|
||||
type="text"
|
||||
bind:value={$form.name}
|
||||
{...$constraints.name}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="btn btn-primary max-w-32"
|
||||
disabled={browser && station && $tainted === undefined}
|
||||
type="submit"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<slot />
|
||||
</div>
|
||||
</form>
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
export let category: Category;
|
||||
export let baseUrl = URL_ENTRIES;
|
||||
export let href: string | undefined = undefined;
|
||||
|
||||
$: textColor = category.color
|
||||
? colorToHex(getTextColor(hexToColor(category.color)))
|
||||
|
@ -19,26 +18,17 @@
|
|||
category: [{ id: category.id, name: category.name }],
|
||||
},
|
||||
},
|
||||
baseUrl,
|
||||
baseUrl
|
||||
);
|
||||
e.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
style:color={category.color ? textColor : undefined}
|
||||
style:background-color={category.color}
|
||||
class="badge ellipsis"
|
||||
class:badge-neutral={!category.color}
|
||||
{href}>{category.name}</a
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
style:color={category.color ? textColor : undefined}
|
||||
style:background-color={category.color}
|
||||
class="badge ellipsis"
|
||||
class:badge-neutral={!category.color}
|
||||
style={category.color
|
||||
? `color: ${textColor}; background-color: #${category.color};`
|
||||
: undefined}
|
||||
on:click={onClick}>{category.name}</button
|
||||
>
|
||||
{/if}
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
|
||||
import CategoryField from "./CategoryField.svelte";
|
||||
import PatientField from "./PatientField.svelte";
|
||||
import UserField from "./UserField.svelte";
|
||||
import RoomField from "./RoomField.svelte";
|
||||
import SortHeader from "./SortHeader.svelte";
|
||||
import UserField from "./UserField.svelte";
|
||||
|
||||
export let entries: RouterOutput["entry"]["list"];
|
||||
export let sortData: SortRequest | undefined;
|
||||
export let sortData: SortRequest | undefined = undefined;
|
||||
export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
|
||||
export let perPatient = false;
|
||||
export let baseUrl: string;
|
||||
|
@ -20,22 +20,22 @@
|
|||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortHeader key="id" {sortData} {sortUpdate} title="ID" />
|
||||
<SortHeader title="ID" key="id" {sortData} {sortUpdate} />
|
||||
{#if !perPatient}
|
||||
<SortHeader key="patient" {sortData} {sortUpdate} title="Patient" />
|
||||
<SortHeader key="room" {sortData} {sortUpdate} title="Zimmer" />
|
||||
<SortHeader title="Patient" key="patient" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Zimmer" key="room" {sortData} {sortUpdate} />
|
||||
{/if}
|
||||
<SortHeader key="category" {sortData} {sortUpdate} title="Kategorie" />
|
||||
<SortHeader key="created_at" {sortData} {sortUpdate} title="Erstellt am" />
|
||||
<SortHeader key="updated_at" {sortData} {sortUpdate} title="Aktualisiert am" />
|
||||
<SortHeader key="date" {sortData} {sortUpdate} title="Zu erledigen am" />
|
||||
<SortHeader key="author" {sortData} {sortUpdate} title="Autor" />
|
||||
<SortHeader key="version_text" {sortData} {sortUpdate} title="Beschreibung" />
|
||||
<SortHeader key="executed_at" {sortData} {sortUpdate} title="Erledigt" />
|
||||
<SortHeader title="Kategorie" key="category" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Erstellt am" key="created_at" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Aktualisiert am" key="updated_at" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Zu erledigen am" key="date" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Autor" key="author" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Beschreibung" key="version_text" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Erledigt" key="executed_at" {sortData} {sortUpdate} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each entries.items as entry (entry.id)}
|
||||
{#each entries.items as entry}
|
||||
<tr
|
||||
class="transition-colors hover:bg-neutral-content/10"
|
||||
class:done={entry.execution}
|
||||
|
@ -44,35 +44,35 @@
|
|||
<td
|
||||
><a
|
||||
class="btn btn-xs btn-primary"
|
||||
aria-label="Eintrag anzeigen"
|
||||
href="/entry/{entry.id}">{entry.id}</a
|
||||
href="/entry/{entry.id}"
|
||||
aria-label="Eintrag anzeigen">{entry.id}</a
|
||||
></td
|
||||
>
|
||||
{#if !perPatient}
|
||||
<td><PatientField {baseUrl} patient={entry.patient} /></td>
|
||||
<td><PatientField patient={entry.patient} {baseUrl} /></td>
|
||||
<td>
|
||||
{#if entry.patient.room}
|
||||
<RoomField {baseUrl} room={entry.patient.room} />
|
||||
<RoomField room={entry.patient.room} {baseUrl} />
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
<td>
|
||||
{#if entry.current_version.category}
|
||||
<CategoryField {baseUrl} category={entry.current_version.category} />
|
||||
<CategoryField category={entry.current_version.category} {baseUrl} />
|
||||
{/if}
|
||||
</td>
|
||||
<td>{formatDate(entry.created_at, true)}</td>
|
||||
<td>{formatDate(entry.current_version.created_at, true)}</td>
|
||||
<td>{formatDate(entry.current_version.date)}</td>
|
||||
<td><UserField {baseUrl} user={entry.current_version.author} /></td>
|
||||
<td><UserField user={entry.current_version.author} {baseUrl} /></td>
|
||||
<td><span class="line-clamp-2">{entry.current_version.text}</span></td>
|
||||
<td>
|
||||
{#if entry.execution}
|
||||
{formatDate(entry.execution.created_at, true)}
|
||||
<UserField
|
||||
{baseUrl}
|
||||
filterName="executor"
|
||||
user={entry.execution.author}
|
||||
filterName="executor"
|
||||
{baseUrl}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||
import type { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||
import { type RouterOutput } from "$lib/shared/trpc";
|
||||
import { getQueryUrl } from "$lib/shared/util";
|
||||
|
||||
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
||||
import { ENTRY_FILTERS } from "$lib/components/filter/filters";
|
||||
import type { FilterQdata } from "$lib/components/filter/types";
|
||||
import EntryTable from "$lib/components/table/EntryTable.svelte";
|
||||
import PaginationButtons from "$lib/components/ui/PaginationButtons.svelte";
|
||||
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
||||
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||
import type { FilterQdata } from "$lib/components/filter/types";
|
||||
import { type RouterOutput } from "$lib/shared/trpc";
|
||||
import { ENTRY_FILTERS } from "$lib/components/filter/filters";
|
||||
import { getQueryUrl } from "$lib/shared/util";
|
||||
import type { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||
import { z } from "zod";
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
export let query: z.infer<typeof ZEntriesQuery>;
|
||||
export let entries: RouterOutput["entry"]["list"];
|
||||
|
@ -54,23 +50,23 @@
|
|||
|
||||
<FilterBar
|
||||
FILTERS={ENTRY_FILTERS}
|
||||
filterData={query.filter}
|
||||
hiddenFilters={patientId !== null ? ["patient"] : []}
|
||||
filterData={query.filter}
|
||||
onUpdate={filterUpdate}
|
||||
>
|
||||
<slot />
|
||||
</FilterBar>
|
||||
|
||||
<EntryTable
|
||||
{baseUrl}
|
||||
{entries}
|
||||
perPatient={patientId !== null}
|
||||
sortData={query.sort}
|
||||
{sortUpdate}
|
||||
perPatient={patientId !== null}
|
||||
{baseUrl}
|
||||
/>
|
||||
|
||||
<PaginationButtons
|
||||
paginationData={query.pagination}
|
||||
data={entries}
|
||||
onUpdate={paginationUpdate}
|
||||
paginationData={query.pagination}
|
||||
/>
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||
import type { ZPatientsQuery } from "$lib/shared/model/validation";
|
||||
import { type RouterOutput } from "$lib/shared/trpc";
|
||||
import { getQueryUrl } from "$lib/shared/util";
|
||||
|
||||
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
||||
import { PATIENT_FILTER } from "$lib/components/filter/filters";
|
||||
import type { FilterQdata } from "$lib/components/filter/types";
|
||||
import PatientTable from "$lib/components/table/PatientTable.svelte";
|
||||
import PaginationButtons from "$lib/components/ui/PaginationButtons.svelte";
|
||||
import FilterBar from "$lib/components/filter/FilterBar.svelte";
|
||||
import type { PaginationRequest, SortRequest } from "$lib/shared/model";
|
||||
import type { FilterQdata } from "$lib/components/filter/types";
|
||||
import { type RouterOutput } from "$lib/shared/trpc";
|
||||
import { PATIENT_FILTER } from "$lib/components/filter/filters";
|
||||
import { getQueryUrl } from "$lib/shared/util";
|
||||
import type { ZPatientsQuery } from "$lib/shared/model/validation";
|
||||
import { z } from "zod";
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
export let query: z.infer<typeof ZPatientsQuery>;
|
||||
export let patients: RouterOutput["patient"]["list"];
|
||||
|
@ -58,10 +54,10 @@
|
|||
<slot />
|
||||
</FilterBar>
|
||||
|
||||
<PatientTable {baseUrl} {patients} sortData={query.sort} {sortUpdate} />
|
||||
<PatientTable {patients} sortData={query.sort} {sortUpdate} {baseUrl} />
|
||||
|
||||
<PaginationButtons
|
||||
paginationData={query.pagination}
|
||||
data={patients}
|
||||
onUpdate={paginationUpdate}
|
||||
paginationData={query.pagination}
|
||||
/>
|
||||
|
|
|
@ -14,14 +14,12 @@
|
|||
],
|
||||
},
|
||||
},
|
||||
baseUrl,
|
||||
baseUrl
|
||||
);
|
||||
e.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="ellipsis"
|
||||
on:click={onClick}
|
||||
<button class="ellipsis" on:click={onClick}
|
||||
>{`${patient.first_name} ${patient.last_name}`}</button
|
||||
>
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { mdiClose, mdiFilter } from "@mdi/js";
|
||||
|
||||
import { URL_ENTRIES } from "$lib/shared/constants";
|
||||
import type { SortRequest } from "$lib/shared/model";
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
import { formatDate, gotoEntityQuery } from "$lib/shared/util";
|
||||
|
||||
import { mdiClose, mdiFilter } from "@mdi/js";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
|
||||
import RoomField from "./RoomField.svelte";
|
||||
import SortHeader from "./SortHeader.svelte";
|
||||
import { URL_ENTRIES } from "$lib/shared/constants";
|
||||
|
||||
export let patients: RouterOutput["patient"]["list"];
|
||||
export let sortData: SortRequest | undefined;
|
||||
export let sortData: SortRequest | undefined = undefined;
|
||||
export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
|
||||
export let baseUrl: string;
|
||||
</script>
|
||||
|
@ -21,17 +19,17 @@
|
|||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortHeader key="id" {sortData} {sortUpdate} title="ID" />
|
||||
<SortHeader key="name" {sortData} {sortUpdate} title="Name" />
|
||||
<SortHeader key="age" {sortData} {sortUpdate} title="Alter" />
|
||||
<SortHeader key="room" {sortData} {sortUpdate} title="Zimmer" />
|
||||
<SortHeader key="created_at" {sortData} {sortUpdate} title="Erstellt am" />
|
||||
<SortHeader title="ID" key="id" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Name" key="name" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Alter" key="age" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Zimmer" key="room" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Erstellt am" key="created_at" {sortData} {sortUpdate} />
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each patients.items as patient (patient.id)}
|
||||
{@const full_name = `${patient.first_name} ${patient.last_name}`}
|
||||
{#each patients.items as patient}
|
||||
{@const full_name = patient.first_name + " " + patient.last_name}
|
||||
<tr
|
||||
class="transition-colors hover:bg-neutral-content/10"
|
||||
class:p-hidden={patient.hidden}
|
||||
|
@ -39,15 +37,15 @@
|
|||
<td
|
||||
><a
|
||||
class="btn btn-xs btn-primary"
|
||||
aria-label="Eintrag anzeigen"
|
||||
href="/patient/{patient.id}">{patient.id}</a
|
||||
href="/patient/{patient.id}"
|
||||
aria-label="Eintrag anzeigen">{patient.id}</a
|
||||
></td
|
||||
>
|
||||
<td>{full_name}</td>
|
||||
<td>{patient.age ?? ""}</td>
|
||||
<td>
|
||||
{#if patient.room}
|
||||
<RoomField {baseUrl} room={patient.room} />
|
||||
<RoomField room={patient.room} {baseUrl} />
|
||||
{/if}
|
||||
</td>
|
||||
<td>{formatDate(patient.created_at, true)}</td>
|
||||
|
@ -57,14 +55,14 @@
|
|||
on:click={() => {
|
||||
gotoEntityQuery(
|
||||
{ filter: { patient: [{ id: patient.id, name: full_name }] } },
|
||||
URL_ENTRIES,
|
||||
URL_ENTRIES
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiFilter} size={1.2} />
|
||||
<Icon size={1.2} path={mdiFilter} />
|
||||
</button>
|
||||
<button class="btn btn-circle btn-ghost btn-xs inline">
|
||||
<Icon path={mdiClose} size={1.2} />
|
||||
<Icon size={1.2} path={mdiClose} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
room: [{ id: room.id, name: room.name }],
|
||||
},
|
||||
},
|
||||
baseUrl,
|
||||
baseUrl
|
||||
);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { mdiSortAscending, mdiSortDescending } from "@mdi/js";
|
||||
|
||||
import type { SortRequest } from "$lib/shared/model";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import { mdiSortAscending, mdiSortDescending } from "@mdi/js";
|
||||
|
||||
export let key: string;
|
||||
export let title: string;
|
||||
export let sortData: SortRequest | undefined;
|
||||
export let sortData: SortRequest | undefined = undefined;
|
||||
export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
|
||||
|
||||
// 1: asc, 2: desc, 0: not sorted
|
||||
let sorting = 0;
|
||||
$: if (sortData?.field === key) {
|
||||
sorting = sortData.asc !== false ? 1 : 2;
|
||||
} else {
|
||||
sorting = 0;
|
||||
}
|
||||
$: sorting = sortData?.field === key ? (sortData.asc !== false ? 1 : 2) : 0;
|
||||
|
||||
function onClick() {
|
||||
if (sorting === 2) {
|
||||
|
@ -30,7 +23,7 @@
|
|||
<th>
|
||||
<button class:text-primary={sorting > 0} on:click={onClick}>
|
||||
{#if sorting > 0}
|
||||
<Icon path={sorting === 1 ? mdiSortAscending : mdiSortDescending} size={1} />
|
||||
<Icon size={1} path={sorting === 1 ? mdiSortAscending : mdiSortDescending} />
|
||||
{/if}
|
||||
{title}
|
||||
</button>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
export let filterName: string = "author";
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
const query: EntityQuery = { filter: {} };
|
||||
let query: EntityQuery = { filter: {} };
|
||||
// @ts-expect-error filterName is checked
|
||||
query.filter[filterName] = [{ id: user.id, name: user.name ?? "" }];
|
||||
gotoEntityQuery(query, baseUrl);
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
|
||||
<label class="form-control w-full max-w-xs">
|
||||
<div class="label">{label}</div>
|
||||
<input {name} class="input input-bordered w-full max-w-xs" {type} {value} />
|
||||
<input {name} {type} class="input input-bordered w-full max-w-xs" {value} />
|
||||
</label>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import type { TRPCErrorResponse } from "$lib/shared/model";
|
||||
|
||||
export let error: TRPCErrorResponse;
|
||||
export let status: number | undefined;
|
||||
export let status: number | undefined = undefined;
|
||||
export let withBtn = false;
|
||||
|
||||
$: statusCode = status ?? error.data?.httpStatus;
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<div class="flex flex-row">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{#if backHref}
|
||||
<a class="btn btn-sm btn-circle btn-ghost" href={backHref}>
|
||||
<a href={backHref} class="btn btn-sm btn-circle btn-ghost">
|
||||
<Icon path={mdiChevronLeft} size={1.8} />
|
||||
</a>
|
||||
{/if}
|
||||
|
|
|
@ -15,14 +15,10 @@
|
|||
</script>
|
||||
|
||||
<svg
|
||||
style:--color={color}
|
||||
style:--size="{size}rem"
|
||||
style:--scaleV={scaleV}
|
||||
style:--spin-duration="{absSpin}s"
|
||||
style:--scaleH={scaleH}
|
||||
style:--rotate="{rotate}deg"
|
||||
class={cls}
|
||||
viewBox="0 0 24 24"
|
||||
style="--color: {color}; --size: {size}rem; --rotate: {rotate}deg; --scaleV: {scaleV}; --scaleH:
|
||||
{scaleH}; --spin-duration: {absSpin}s;"
|
||||
class={cls}
|
||||
>
|
||||
{#if title}<title>{title}</title>{/if}
|
||||
|
||||
|
@ -31,7 +27,7 @@
|
|||
</g>
|
||||
</svg>
|
||||
|
||||
<style lang="postcss">
|
||||
<style>
|
||||
svg {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
|
@ -40,14 +36,12 @@
|
|||
height: var(--size);
|
||||
transform: rotate(var(--rotate)) scale(var(--scaleV), var(--scaleH));
|
||||
}
|
||||
/* If spin is strictly > 0,
|
||||
class spin is added to g and plays {spinFrames} in {spin-duration} seconds */
|
||||
/* If spin is strictly > 0, class spin is added to g and plays {spinFrames} in {spin-duration} seconds */
|
||||
g.spin {
|
||||
transform-origin: center;
|
||||
animation: spinFrames linear var(--spin-duration) infinite;
|
||||
}
|
||||
/* If spin is strictly < 0,
|
||||
class spinReverse is added to g and plays {spinReverseFrames} in {spin-duration} seconds */
|
||||
/* If spin is strictly < 0, class spinReverse is added to g and plays {spinReverseFrames} in {spin-duration} seconds */
|
||||
g.spinReverse {
|
||||
transform-origin: center;
|
||||
animation: spinReverseFrames linear var(--spin-duration) infinite;
|
||||
|
|
|
@ -12,8 +12,7 @@
|
|||
showProgress = true;
|
||||
showError = false;
|
||||
navInterval = setInterval(() => {
|
||||
if (navprogress <= 90) navprogress += 2.5;
|
||||
else if (navprogress < 95) navprogress += 0.1;
|
||||
navprogress += navprogress <= 90 ? 2.5 : navprogress < 95 ? 0.1 : 0;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
@ -37,8 +36,8 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
style:width={`${navprogress}%`}
|
||||
class={cls}
|
||||
style:width={`${navprogress}%`}
|
||||
class:active={alwaysShown || showProgress}
|
||||
class:error={showError}
|
||||
/>
|
||||
|
|
13
src/lib/components/ui/Markdown.svelte
Normal file
13
src/lib/components/ui/Markdown.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { marked } from "marked";
|
||||
|
||||
export let src: string;
|
||||
|
||||
$: doc = DOMPurify.sanitize(marked.parse(src) as string, { FORBID_TAGS: ["img"] });
|
||||
</script>
|
||||
|
||||
<p class="prose">
|
||||
<!--eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html doc}
|
||||
</p>
|
|
@ -1,11 +1,7 @@
|
|||
<script lang="ts">
|
||||
import "./carta.pcss";
|
||||
|
||||
import { Carta, MarkdownEditor, type Labels } from "carta-md";
|
||||
import Markdown from "./Markdown.svelte";
|
||||
import type { InputConstraint } from "sveltekit-superforms";
|
||||
|
||||
import { CARTA_CFG } from "./carta";
|
||||
|
||||
export let label = "";
|
||||
export let name = "";
|
||||
export let value = "";
|
||||
|
@ -14,34 +10,32 @@
|
|||
export let constraints: InputConstraint | undefined = undefined;
|
||||
export let marginTop = false;
|
||||
|
||||
const carta = new Carta(CARTA_CFG);
|
||||
let editMode = true;
|
||||
|
||||
const LABELS: Labels = {
|
||||
writeTab: "Bearbeiten",
|
||||
previewTab: "Vorschau",
|
||||
iconsLabels: {
|
||||
heading: "Titel",
|
||||
bold: "Fett",
|
||||
italic: "Kursiv",
|
||||
strikethrough: "Durchgestrichen",
|
||||
quote: "Zitat",
|
||||
bulletedList: "Aufzählung",
|
||||
numberedList: "Nummerierung",
|
||||
},
|
||||
};
|
||||
function toggle() {
|
||||
editMode = !editMode;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card2" class:mt-4={marginTop}>
|
||||
<div class="row c-light text-sm items-center justify-between">
|
||||
<span>{label}</span>
|
||||
<button type="button" class="label-text" on:click={toggle} tabindex="-1">
|
||||
{editMode ? "Vorschau" : "Bearbeiten"}
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<MarkdownEditor
|
||||
{carta}
|
||||
labels={LABELS}
|
||||
mode="tabs"
|
||||
textarea={{ name, "aria-invalid": ariaInvalid, ...constraints }}
|
||||
bind:value />
|
||||
{#if editMode}
|
||||
<textarea
|
||||
class="textarea w-full h-48"
|
||||
{name}
|
||||
aria-invalid={ariaInvalid}
|
||||
bind:value
|
||||
{...constraints}
|
||||
/>
|
||||
{:else}
|
||||
<Markdown src={value} />
|
||||
{/if}
|
||||
|
||||
{#if errors}
|
||||
<div class="label flex-col items-start">
|
|
@ -5,8 +5,7 @@
|
|||
|
||||
<div>
|
||||
<a
|
||||
class="btn btn-sm btn-ghost drawer-button font-normal
|
||||
decoration-primary decoration-4 underline-offset-4"
|
||||
class="btn btn-sm btn-ghost drawer-button font-normal decoration-primary decoration-4 underline-offset-4"
|
||||
class:underline={active}
|
||||
{href}><slot /></a
|
||||
>
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
mdiChevronLeft, mdiChevronRight, mdiPageFirst, mdiPageLast,
|
||||
} from "@mdi/js";
|
||||
import { mdiChevronLeft, mdiChevronRight, mdiPageFirst, mdiPageLast } from "@mdi/js";
|
||||
|
||||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
||||
import type { Pagination, PaginationRequest } from "$lib/shared/model";
|
||||
|
||||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
export let paginationData: PaginationRequest | null | undefined;
|
||||
export let paginationData: PaginationRequest | null | undefined = undefined;
|
||||
export let data: Pagination<unknown>;
|
||||
export let onUpdate: (pagination: PaginationRequest) => void = () => {};
|
||||
|
||||
|
@ -34,13 +31,13 @@
|
|||
function getPaginationRequest(page: number): PaginationRequest | null {
|
||||
if (page < 1 || page > nPages) return null;
|
||||
|
||||
const pag = paginationData ? structuredClone(paginationData) : {};
|
||||
let pag = paginationData ? structuredClone(paginationData) : {};
|
||||
pag.offset = (page - 1) * limit;
|
||||
return pag;
|
||||
}
|
||||
|
||||
function pagClick(page: number) {
|
||||
const pag = getPaginationRequest(page);
|
||||
let pag = getPaginationRequest(page);
|
||||
if (pag) onUpdate(pag);
|
||||
}
|
||||
</script>
|
||||
|
@ -54,9 +51,7 @@
|
|||
<button class="join-item btn btn-sm" on:click={() => pagClick(1)}>
|
||||
<Icon path={mdiPageFirst} />
|
||||
</button>
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
on:click={() => pagClick(thisPage - 1)}
|
||||
<button class="join-item btn btn-sm" on:click={() => pagClick(thisPage - 1)}
|
||||
><Icon path={mdiChevronLeft} /></button
|
||||
>
|
||||
{#each { length: windowTop + 1 - windowBottom } as _, i}
|
||||
|
@ -67,14 +62,10 @@
|
|||
on:click={() => pagClick(n)}>{n}</button
|
||||
>
|
||||
{/each}
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
on:click={() => pagClick(thisPage + 1)}
|
||||
<button class="join-item btn btn-sm" on:click={() => pagClick(thisPage + 1)}
|
||||
><Icon path={mdiChevronRight} /></button
|
||||
>
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
on:click={() => pagClick(nPages)}
|
||||
<button class="join-item btn btn-sm" on:click={() => pagClick(nPages)}
|
||||
><Icon path={mdiPageLast} /></button
|
||||
>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<!-- Button showing the amount of entry versions -->
|
||||
<script lang="ts">
|
||||
import { mdiHistory } from "@mdi/js";
|
||||
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
export let n: number;
|
||||
|
@ -9,7 +8,7 @@
|
|||
</script>
|
||||
|
||||
{#if n > 1}
|
||||
<a class="btn btn-xs btn-primary rounded-full" {href}>
|
||||
<a {href} class="btn btn-xs btn-primary rounded-full">
|
||||
<Icon path={mdiHistory} size={1.2} />
|
||||
<span>{n}</span>
|
||||
</a>
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { Carta, Markdown } from "carta-md";
|
||||
|
||||
import { CARTA_CFG } from "./carta";
|
||||
|
||||
export let src: string;
|
||||
const carta = new Carta(CARTA_CFG);
|
||||
</script>
|
||||
|
||||
<Markdown {carta} value={src} />
|
|
@ -1,123 +0,0 @@
|
|||
.carta-theme__default.carta-editor {
|
||||
@apply border border-solid border-base-content/30 rounded-xl;
|
||||
}
|
||||
|
||||
/* Box sizings */
|
||||
.carta-theme__default .carta-toolbar {
|
||||
@apply border-b border-solid border-base-content/30;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-wrapper {
|
||||
@apply px-4;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-container > * {
|
||||
@apply my-4;
|
||||
}
|
||||
|
||||
/* Text settings */
|
||||
.carta-theme__default .carta-input {
|
||||
@apply caret-base-content text-base;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-input ::placeholder {
|
||||
@apply text-base-content;
|
||||
}
|
||||
|
||||
/* Splitter */
|
||||
.carta-theme__default .mode-split.carta-container::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 1px;
|
||||
@apply bg-base-content/30;
|
||||
}
|
||||
|
||||
.carta-theme__default .mode-split .carta-input {
|
||||
@apply pr-4;
|
||||
}
|
||||
|
||||
.carta-theme__default .mode-split .carta-renderer {
|
||||
@apply pl-4;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.carta-theme__default .carta-toolbar {
|
||||
@apply px-3;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-toolbar-left {
|
||||
@apply flex items-end;
|
||||
}
|
||||
|
||||
.carta-theme__default button {
|
||||
@apply text-base-content;
|
||||
}
|
||||
|
||||
/* Markdown input and renderer */
|
||||
.carta-theme__default .carta-input,
|
||||
.carta-theme__default .carta-renderer {
|
||||
height: 300px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.carta-theme__default .carta-icon,
|
||||
.carta-theme__default .carta-icon-full {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
@apply btn-transition;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-icon-full {
|
||||
padding: 6px 4px;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-icon-full span {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-icon:hover,
|
||||
.carta-theme__default .carta-icon-full:hover {
|
||||
@apply bg-base-content/20;
|
||||
}
|
||||
|
||||
.carta-input > pre {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-icons-menu {
|
||||
@apply border border-solid border-base-content/30;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-icons-menu .carta-icon-full {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-icons-menu .carta-icon-full:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.carta-theme__default .carta-toolbar-left button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-right: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-toolbar-left button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.carta-theme__default .carta-toolbar-left button.carta-active {
|
||||
@apply font-semibold border-primary;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import type { Options } from "carta-md";
|
||||
|
||||
import { sanitizeHtml } from "$lib/shared/util";
|
||||
|
||||
export const CARTA_CFG: Options = {
|
||||
theme: "github-dark",
|
||||
sanitizer: sanitizeHtml,
|
||||
disableIcons: ["taskList"],
|
||||
};
|
|
@ -7,13 +7,12 @@ import {
|
|||
type AuthConfig,
|
||||
} from "@auth/core";
|
||||
import Keycloak from "@auth/core/providers/keycloak";
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
import { PrismaAdapter } from "$lib/server/authAdapter";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import { redirect, type Handle, type RequestEvent } from "@sveltejs/kit";
|
||||
import { parse } from "set-cookie-parser";
|
||||
|
||||
import { env } from "$env/dynamic/private";
|
||||
import { PrismaAdapter } from "$lib/server/authAdapter";
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
const AUTH_BASE_PATH: string = "/auth";
|
||||
|
||||
export const AUTH_CFG: AuthConfig = {
|
||||
|
@ -62,14 +61,14 @@ ISC License
|
|||
*/
|
||||
|
||||
function authjsUrl(event: RequestEvent, authjsEndpoint: string): string {
|
||||
const { url } = event;
|
||||
return `${url.protocol}//${url.host}${AUTH_BASE_PATH}/${authjsEndpoint}`;
|
||||
const url = event.url;
|
||||
return url.protocol + "//" + url.host + AUTH_BASE_PATH + "/" + authjsEndpoint;
|
||||
}
|
||||
|
||||
export async function makeAuthjsRequest(
|
||||
event: RequestEvent,
|
||||
authjsEndpoint: string,
|
||||
params: Record<string, string>,
|
||||
params: Record<string, string>
|
||||
) {
|
||||
const headers = new Headers(event.request.headers);
|
||||
headers.set("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
@ -113,12 +112,12 @@ export const skAuthHandle: Handle = async ({ event, resolve }) => {
|
|||
const { url, request } = event;
|
||||
|
||||
if (
|
||||
url.pathname.startsWith(`${AUTH_BASE_PATH}/`)
|
||||
&& isAuthAction(url.pathname.slice(AUTH_BASE_PATH.length + 1).split("/")[0])
|
||||
url.pathname.startsWith(AUTH_BASE_PATH + "/") &&
|
||||
isAuthAction(url.pathname.slice(AUTH_BASE_PATH.length + 1).split("/")[0])
|
||||
) {
|
||||
return Auth(request, AUTH_CFG);
|
||||
}
|
||||
} else {
|
||||
event.locals.session = await auth(event);
|
||||
|
||||
}
|
||||
return resolve(event);
|
||||
};
|
||||
|
|
|
@ -33,16 +33,19 @@ function mapAccount(account: Account): AdapterAccount {
|
|||
|
||||
export function PrismaAdapter(p: PrismaClient): Adapter {
|
||||
return {
|
||||
createUser: async (data) => mapUser(
|
||||
createUser: async (data) =>
|
||||
mapUser(
|
||||
await p.user.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
},
|
||||
}),
|
||||
})
|
||||
),
|
||||
getUser: async (id) => mapUserOpt(await p.user.findUnique({ where: { id: Number(id) } })),
|
||||
getUserByEmail: async (email) => mapUserOpt(await p.user.findUnique({ where: { email } })),
|
||||
getUser: async (id) =>
|
||||
mapUserOpt(await p.user.findUnique({ where: { id: Number(id) } })),
|
||||
getUserByEmail: async (email) =>
|
||||
mapUserOpt(await p.user.findUnique({ where: { email } })),
|
||||
async getUserByAccount(provider_providerAccountId) {
|
||||
const account = await p.account.findUnique({
|
||||
where: { provider_providerAccountId },
|
||||
|
@ -50,14 +53,12 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
|
|||
});
|
||||
return mapUserOpt(account?.user) ?? null;
|
||||
},
|
||||
updateUser: async ({ id, ...data }) => mapUser(
|
||||
await p.user.update({
|
||||
where: { id: Number(id) },
|
||||
data,
|
||||
}),
|
||||
),
|
||||
deleteUser: async (id) => mapUser(await p.user.delete({ where: { id: Number(id) } })),
|
||||
linkAccount: async (data) => mapAccount(
|
||||
updateUser: async ({ id, ...data }) =>
|
||||
mapUser(await p.user.update({ where: { id: Number(id) }, data })),
|
||||
deleteUser: async (id) =>
|
||||
mapUser(await p.user.delete({ where: { id: Number(id) } })),
|
||||
linkAccount: async (data) =>
|
||||
mapAccount(
|
||||
await p.account.create({
|
||||
data: {
|
||||
user_id: Number(data.userId),
|
||||
|
@ -71,9 +72,10 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
|
|||
scope: data.scope,
|
||||
id_token: data.id_token,
|
||||
},
|
||||
}),
|
||||
})
|
||||
),
|
||||
unlinkAccount: (provider_providerAccountId) => p.account.delete({
|
||||
unlinkAccount: (provider_providerAccountId) =>
|
||||
p.account.delete({
|
||||
where: { provider_providerAccountId },
|
||||
}) as unknown as AdapterAccount,
|
||||
};
|
||||
|
|
|
@ -2,8 +2,9 @@ import { PrismaClient } from "@prisma/client";
|
|||
|
||||
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
||||
|
||||
export const prisma = globalForPrisma.prisma
|
||||
|| new PrismaClient({
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ||
|
||||
new PrismaClient({
|
||||
// log: ["query"],
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type { Category, CategoryNew } from "$lib/shared/model";
|
||||
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
export async function newCategory(category: CategoryNew): Promise<number> {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { prisma } from "$lib/server/prisma";
|
||||
import type {
|
||||
EntriesFilter,
|
||||
Entry,
|
||||
|
@ -12,9 +13,6 @@ import type {
|
|||
} from "$lib/shared/model";
|
||||
import { dateToYMD } from "$lib/shared/util";
|
||||
import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error";
|
||||
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
import { mapEntry, mapVersion, mapExecution } from "./mapping";
|
||||
import { QueryBuilder, filterListToArray, parseSearchQuery } from "./util";
|
||||
|
||||
|
@ -88,7 +86,7 @@ export async function newEntryVersion(
|
|||
author_id: number,
|
||||
entry_id: number,
|
||||
version: EntryVersionNew,
|
||||
old_version_id: number | undefined = undefined,
|
||||
old_version_id: number | undefined = undefined
|
||||
): Promise<number> {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const entry = await tx.entry.findUniqueOrThrow({
|
||||
|
@ -119,10 +117,10 @@ export async function newEntryVersion(
|
|||
|
||||
// Check if there are any updates
|
||||
if (
|
||||
cver?.text === updatedVersion.text
|
||||
&& cver?.date.getTime() === updatedVersion.date.getTime()
|
||||
&& cver?.category_id === updatedVersion.category_id
|
||||
&& cver?.priority === updatedVersion.priority
|
||||
cver?.text === updatedVersion.text &&
|
||||
cver?.date.getTime() === updatedVersion.date.getTime() &&
|
||||
cver?.category_id === updatedVersion.category_id &&
|
||||
cver?.priority === updatedVersion.priority
|
||||
) {
|
||||
return cver.id;
|
||||
}
|
||||
|
@ -139,7 +137,7 @@ export async function newEntryExecution(
|
|||
author_id: number,
|
||||
entry_id: number,
|
||||
execution: EntryExecutionNew,
|
||||
old_execution_id: number | null | undefined = undefined,
|
||||
old_execution_id: number | null | undefined = undefined
|
||||
): Promise<number> {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const entry = await tx.entry.findUniqueOrThrow({
|
||||
|
@ -155,8 +153,8 @@ export async function newEntryExecution(
|
|||
|
||||
// Check if the execution has been updated by someone else
|
||||
if (
|
||||
(old_execution_id && (!cex || cex.id !== old_execution_id))
|
||||
|| (old_execution_id === null && cex)
|
||||
(old_execution_id && (!cex || cex.id !== old_execution_id)) ||
|
||||
(old_execution_id === null && cex)
|
||||
) {
|
||||
throw new ErrorConflict("old execution id does not match");
|
||||
}
|
||||
|
@ -181,7 +179,7 @@ export async function newEntryExecution(
|
|||
export async function getEntries(
|
||||
filter: EntriesFilter = {},
|
||||
pagination: PaginationRequest = {},
|
||||
sort: SortRequest = { field: "created_at", asc: false },
|
||||
sort: SortRequest = { field: "created_at", asc: false }
|
||||
): Promise<Pagination<Entry>> {
|
||||
const qb = new QueryBuilder(
|
||||
`select
|
||||
|
@ -242,14 +240,14 @@ left join entry_executions ex on
|
|||
left join users xau on xau.id=ex.author_id
|
||||
join patients p on p.id = e.patient_id
|
||||
left join rooms r on r.id = p.room_id
|
||||
left join stations s on s.id = r.station_id`,
|
||||
left join stations s on s.id = r.station_id`
|
||||
);
|
||||
|
||||
if (filter?.search && filter.search.length > 0) {
|
||||
const query = parseSearchQuery(filter.search);
|
||||
qb.addFilterClause(
|
||||
`to_tsquery('german', ${qb.pvar()}) @@ e.tsvec`,
|
||||
query.toTsquery(),
|
||||
query.toTsquery()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -270,7 +268,7 @@ left join stations s on s.id = r.station_id`,
|
|||
const author = filterListToArray(filter?.author);
|
||||
qb.addFilterClause(
|
||||
`(${qb.pvar()})::integer[] && (select array_agg(ev2.author_id) from entry_versions ev2 where ev2.entry_id=e.id)`,
|
||||
author,
|
||||
author
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -332,7 +330,8 @@ left join stations s on s.id = r.station_id`,
|
|||
])) as [RowItem[], { count: bigint }[]];
|
||||
|
||||
const total = Number(countRes[0].count);
|
||||
const items: Entry[] = res.map((item) => ({
|
||||
const items: Entry[] = res.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
patient: {
|
||||
id: item.patient_id,
|
||||
|
@ -374,7 +373,8 @@ left join stations s on s.id = r.station_id`,
|
|||
created_at: item.execution_created_at,
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
return { items, offset: qb.getOffset(), total };
|
||||
}
|
||||
|
|
|
@ -1,14 +1,3 @@
|
|||
import type {
|
||||
Patient as DbPatient,
|
||||
Room as DbRoom,
|
||||
Station as DbStation,
|
||||
User as DbUser,
|
||||
Entry as DbEntry,
|
||||
EntryVersion as DbEntryVersion,
|
||||
EntryExecution as DbEntryExecution,
|
||||
Category as DbCategory,
|
||||
} from "@prisma/client";
|
||||
|
||||
import type {
|
||||
Entry,
|
||||
Patient,
|
||||
|
@ -21,6 +10,16 @@ import type {
|
|||
} from "$lib/shared/model";
|
||||
import { dateToYMD } from "$lib/shared/util";
|
||||
import { ErrorNotFound } from "$lib/shared/util/error";
|
||||
import type {
|
||||
Patient as DbPatient,
|
||||
Room as DbRoom,
|
||||
Station as DbStation,
|
||||
User as DbUser,
|
||||
Entry as DbEntry,
|
||||
EntryVersion as DbEntryVersion,
|
||||
EntryExecution as DbEntryExecution,
|
||||
Category as DbCategory,
|
||||
} from "@prisma/client";
|
||||
|
||||
type DbRoomLn = DbRoom & { station: DbStation };
|
||||
type DbPatientLn = DbPatient & { room: DbRoomLn | null };
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
|
||||
import type {
|
||||
Patient,
|
||||
PatientNew,
|
||||
|
@ -9,12 +7,11 @@ import type {
|
|||
PatientTag,
|
||||
SortRequest,
|
||||
} from "$lib/shared/model";
|
||||
import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error";
|
||||
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import { mapPatient } from "./mapping";
|
||||
import { QueryBuilder } from "./util";
|
||||
import { ErrorConflict, ErrorInvalidInput } from "$lib/shared/util/error";
|
||||
|
||||
export async function newPatient(patient: PatientNew): Promise<number> {
|
||||
const created = await prisma.patient.create({ data: patient, select: { id: true } });
|
||||
|
@ -67,11 +64,13 @@ export async function getPatientNames(): Promise<PatientTag[]> {
|
|||
where: { hidden: false },
|
||||
orderBy: { last_name: "asc" },
|
||||
});
|
||||
return patients.map((p) => ({
|
||||
return patients.map((p) => {
|
||||
return {
|
||||
id: p.id,
|
||||
name: `${p.first_name} ${p.last_name}`,
|
||||
name: p.first_name + " " + p.last_name,
|
||||
room_id: p.room_id,
|
||||
}));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPatientNEntries(id: number): Promise<number> {
|
||||
|
@ -81,21 +80,21 @@ export async function getPatientNEntries(id: number): Promise<number> {
|
|||
export async function getPatients(
|
||||
filter: PatientsFilter = {},
|
||||
pagination: PaginationRequest = {},
|
||||
sort: SortRequest = { field: "created_at", asc: false },
|
||||
sort: SortRequest = { field: "created_at", asc: false }
|
||||
): Promise<Pagination<Patient>> {
|
||||
const qb = new QueryBuilder(
|
||||
`select p.id, p.first_name, p.last_name, p.created_at, p.age, p.hidden,
|
||||
r.id as room_id, r.name as room_name, s.id as station_id, s.name as station_name`,
|
||||
`from patients p
|
||||
left join rooms r on r.id = p.room_id
|
||||
left join stations s on s.id = r.station_id`,
|
||||
left join stations s on s.id = r.station_id`
|
||||
);
|
||||
|
||||
if (filter.search && filter.search.length > 0) {
|
||||
const qvar = qb.pvar();
|
||||
qb.addFilterClause(
|
||||
`(p.full_name % ${qvar} or p.full_name ilike '%' || ${qvar} || '%')`,
|
||||
filter.search,
|
||||
filter.search
|
||||
);
|
||||
qb.addOrderClause(`similarity(p.full_name, ${qvar}) desc`);
|
||||
}
|
||||
|
@ -145,7 +144,8 @@ export async function getPatients(
|
|||
])) as [RowItem[], { count: bigint }[]];
|
||||
|
||||
const total = Number(countRes[0].count);
|
||||
const items: Patient[] = res.map((patient) => ({
|
||||
const items: Patient[] = res.map((patient) => {
|
||||
return {
|
||||
id: patient.id,
|
||||
first_name: patient.first_name,
|
||||
last_name: patient.last_name,
|
||||
|
@ -162,7 +162,8 @@ export async function getPatients(
|
|||
},
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
items,
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import type {
|
||||
RoomNew, Room, Station, StationNew,
|
||||
} from "$lib/shared/model";
|
||||
|
||||
import type { RoomNew, Room, Station, StationNew } from "$lib/shared/model";
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
import { mapRoom } from "./mapping";
|
||||
|
||||
export async function newStation(station: StationNew): Promise<number> {
|
||||
|
@ -20,7 +16,7 @@ export async function deleteStation(id: number) {
|
|||
}
|
||||
|
||||
export async function getStation(id: number): Promise<Station> {
|
||||
return prisma.station.findUniqueOrThrow({ where: { id } });
|
||||
return await prisma.station.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
|
||||
export async function getStations(): Promise<Station[]> {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
||||
import type {
|
||||
Pagination,
|
||||
PaginationRequest,
|
||||
User,
|
||||
UserTagNameNonnull,
|
||||
} from "$lib/shared/model";
|
||||
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
import { mapUser, mapUserTagNameNonnull } from "./mapping";
|
||||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
||||
|
||||
export async function getUser(id: number): Promise<User> {
|
||||
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
|
||||
|
@ -16,7 +14,7 @@ export async function getUser(id: number): Promise<User> {
|
|||
}
|
||||
|
||||
export async function getUsers(
|
||||
pagination: PaginationRequest,
|
||||
pagination: PaginationRequest
|
||||
): Promise<Pagination<User>> {
|
||||
const offset = pagination.offset ?? 0;
|
||||
const [users, total] = await Promise.all([
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, test } from "vitest";
|
||||
|
||||
import { QueryBuilder, parseSearchQuery } from "./util";
|
||||
|
||||
test("query builder", () => {
|
||||
|
@ -10,7 +9,7 @@ test("query builder", () => {
|
|||
|
||||
const query = qb.getQuery();
|
||||
expect(query).toBe(
|
||||
"select e.id, e.text, e.category from entries e where category = any ($1) and text = $2 limit $3 offset $4",
|
||||
"select e.id, e.text, e.category from entries e where category = any ($1) and text = $2 limit $3 offset $4"
|
||||
);
|
||||
|
||||
const params = qb.getParams();
|
||||
|
@ -21,7 +20,7 @@ test("query builder", () => {
|
|||
});
|
||||
|
||||
test("parse search query", () => {
|
||||
const q = '"hello world" banana -cherry -"b x y" vis';
|
||||
const q = `"hello world" banana -cherry -"b x y" vis`;
|
||||
const parsed = parseSearchQuery(q);
|
||||
expect(parsed.components).toMatchObject([
|
||||
{
|
||||
|
@ -53,6 +52,6 @@ test("parse search query", () => {
|
|||
|
||||
const term = parsed.toTsquery();
|
||||
expect(term).toBe(
|
||||
"'hello' <-> 'world' & 'banana' & !'cherry' & !('b' <-> 'x' <-> 'y') & 'vis':*",
|
||||
`'hello' <-> 'world' & 'banana' & !'cherry' & !('b' <-> 'x' <-> 'y') & 'vis':*`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -13,18 +13,18 @@ export function filterListToArray<T>(fl: FilterList<T>): T[] {
|
|||
if (fl[0].id) {
|
||||
// @ts-expect-error checked if id is present
|
||||
return fl.map((itm) => itm.id);
|
||||
}
|
||||
} else {
|
||||
// @ts-expect-error output type checked
|
||||
return fl;
|
||||
}
|
||||
} else {
|
||||
return [fl];
|
||||
}
|
||||
}
|
||||
|
||||
class SearchQueryComponent {
|
||||
word: string;
|
||||
|
||||
typ: QueryComponentType;
|
||||
|
||||
negative: boolean;
|
||||
|
||||
constructor(word: string, typ: QueryComponentType, negative: boolean) {
|
||||
|
@ -49,16 +49,16 @@ class SearchQueryComponent {
|
|||
multipleParts = parts.length > 1;
|
||||
tsquery = parts.join(" <-> ");
|
||||
} else if (this.typ == QueryComponentType.Trailing) {
|
||||
tsquery = `${this.quoted()}:*`;
|
||||
tsquery = this.quoted() + ":*";
|
||||
} else {
|
||||
tsquery = this.quoted();
|
||||
}
|
||||
|
||||
if (this.negative) {
|
||||
if (multipleParts) {
|
||||
tsquery = `!(${tsquery})`;
|
||||
tsquery = "!(" + tsquery + ")";
|
||||
} else {
|
||||
tsquery = `!${tsquery}`;
|
||||
tsquery = "!" + tsquery;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,14 +94,15 @@ export function parseSearchQuery(q: string): SearchQueryComponents {
|
|||
// Exact
|
||||
if (m[2]) {
|
||||
return new SearchQueryComponent(m[2], QueryComponentType.Exact, negative);
|
||||
}
|
||||
} else {
|
||||
return new SearchQueryComponent(m[3], QueryComponentType.Normal, negative);
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
components.length > 0
|
||||
&& components[components.length - 1].typ === QueryComponentType.Normal
|
||||
components.length > 0 &&
|
||||
components[components.length - 1].typ === QueryComponentType.Normal
|
||||
) {
|
||||
components[components.length - 1].typ = QueryComponentType.Trailing;
|
||||
}
|
||||
|
@ -111,19 +112,12 @@ export function parseSearchQuery(q: string): SearchQueryComponents {
|
|||
|
||||
export class QueryBuilder {
|
||||
private selectClause;
|
||||
|
||||
private fromClause;
|
||||
|
||||
private filterClauses: string[] = [];
|
||||
|
||||
private orderClauses: string[] = [];
|
||||
|
||||
private params: unknown[] = [];
|
||||
|
||||
private nP = 0;
|
||||
|
||||
private limit = PAGINATION_LIMIT;
|
||||
|
||||
private offset = 0;
|
||||
|
||||
constructor(selectClause: string, fromClause: string) {
|
||||
|
@ -142,7 +136,7 @@ export class QueryBuilder {
|
|||
|
||||
orderByFields(fields: string[], asc: boolean | undefined = undefined) {
|
||||
const sortDir = asc === false ? " desc" : " asc";
|
||||
const orderClause = fields.join(`${sortDir}, `) + sortDir;
|
||||
const orderClause = fields.join(sortDir + ", ") + sortDir;
|
||||
this.addOrderClause(orderClause);
|
||||
}
|
||||
|
||||
|
@ -174,7 +168,7 @@ export class QueryBuilder {
|
|||
getQuery(): string {
|
||||
const queryParts = [this.selectClause, this.fromClause];
|
||||
if (this.filterClauses.length > 0) {
|
||||
queryParts.push(`where ${this.filterClauses.join(" and ")}`);
|
||||
queryParts.push("where " + this.filterClauses.join(" and "));
|
||||
}
|
||||
|
||||
if (this.orderClauses.length > 0) {
|
||||
|
@ -189,7 +183,7 @@ export class QueryBuilder {
|
|||
getCountQuery(): string {
|
||||
const queryParts = ["select count(*) as count", this.fromClause];
|
||||
if (this.filterClauses.length > 0) {
|
||||
queryParts.push(`where ${this.filterClauses.join(" and ")}`);
|
||||
queryParts.push("where " + this.filterClauses.join(" and "));
|
||||
}
|
||||
return queryParts.join(" ");
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { ZUser } from "$lib/shared/model/validation";
|
||||
import type { RequestEvent } from "@sveltejs/kit";
|
||||
import { type inferAsyncReturnType, TRPCError } from "@trpc/server";
|
||||
|
||||
import { ZUser } from "$lib/shared/model/validation";
|
||||
|
||||
// we're not using the event parameter is this example,
|
||||
// hence the eslint-disable rule
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { ErrorConflict, ErrorNotFound } from "$lib/shared/util/error";
|
||||
import { ZodError } from "zod";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { ErrorConflict, ErrorNotFound } from "$lib/shared/util/error";
|
||||
|
||||
function handleError(error: unknown): never {
|
||||
if (error instanceof ZodError) {
|
||||
|
@ -19,7 +18,7 @@ function handleError(error: unknown): never {
|
|||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: error.message,
|
||||
cause: `Prisma error ${error.code}`,
|
||||
cause: "Prisma error " + error.code,
|
||||
});
|
||||
}
|
||||
} else if (error instanceof ErrorNotFound) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { initTRPC } from "@trpc/server";
|
||||
|
||||
import type { Context } from "./context";
|
||||
|
||||
export { trpcWrap } from "$lib/server/trpc/handleError";
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { ErrorInvalidInput } from "$lib/shared/util/error";
|
||||
|
||||
import { t } from ".";
|
||||
|
||||
import { categoryRouter } from "./routes/category";
|
||||
import { entryRouter } from "./routes/entry";
|
||||
import { patientRouter } from "./routes/patient";
|
||||
import { roomRouter } from "./routes/room";
|
||||
import { stationRouter } from "./routes/station";
|
||||
import { roomRouter } from "./routes/room";
|
||||
import { patientRouter } from "./routes/patient";
|
||||
import { userRouter } from "./routes/user";
|
||||
import { ErrorInvalidInput } from "$lib/shared/util/error";
|
||||
|
||||
export const router = t.router({
|
||||
greeting: t.procedure.query(
|
||||
async () => `Hello tRPC @ ${new Date().toLocaleTimeString()}`,
|
||||
),
|
||||
greeting: t.procedure.query(async () => {
|
||||
return `Hello tRPC @ ${new Date().toLocaleTimeString()}`;
|
||||
}),
|
||||
testError: t.procedure.query(async () => {
|
||||
throw new ErrorInvalidInput("here is your error");
|
||||
}),
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
import { t, trpcWrap } from "..";
|
||||
import { z } from "zod";
|
||||
|
||||
import { fields, ZCategoryNew } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deleteCategory,
|
||||
getCategories,
|
||||
getCategory,
|
||||
newCategory,
|
||||
updateCategory,
|
||||
} from "$lib/server/query";
|
||||
|
||||
import { t, trpcWrap } from "..";
|
||||
import { fields, ZCategoryNew } from "$lib/shared/model/validation";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const categoryRouter = t.router({
|
||||
list: t.procedure.query(async () => trpcWrap(getCategories)),
|
||||
get: t.procedure
|
||||
.input(ZEntityId)
|
||||
.query(async (opts) => trpcWrap(async () => getCategory(opts.input))),
|
||||
create: t.procedure.input(ZCategoryNew).mutation(async (opts) => trpcWrap(async () => {
|
||||
create: t.procedure.input(ZCategoryNew).mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
const id = await newCategory(opts.input);
|
||||
return id;
|
||||
})),
|
||||
return { id };
|
||||
})
|
||||
),
|
||||
update: t.procedure
|
||||
.input(z.object({ id: ZEntityId, category: ZCategoryNew.partial() }))
|
||||
.mutation(async (opts) => trpcWrap(async () => {
|
||||
.mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
await updateCategory(opts.input.id, opts.input.category);
|
||||
})),
|
||||
delete: t.procedure.input(ZEntityId).mutation(async (opts) => trpcWrap(async () => {
|
||||
})
|
||||
),
|
||||
delete: t.procedure.input(ZEntityId).mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
await deleteCategory(opts.input);
|
||||
})),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { t, trpcWrap } from "..";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
|
@ -7,8 +8,6 @@ import {
|
|||
ZEntryNew,
|
||||
ZEntryVersionNew,
|
||||
} from "$lib/shared/model/validation";
|
||||
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
|
||||
|
||||
import {
|
||||
getEntries,
|
||||
getEntry,
|
||||
|
@ -20,70 +19,89 @@ import {
|
|||
newEntryExecution,
|
||||
newEntryVersion,
|
||||
} from "$lib/server/query";
|
||||
|
||||
import { t, trpcWrap } from "..";
|
||||
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const entryRouter = t.router({
|
||||
get: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
||||
get: t.procedure.input(ZEntityId).query(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
const [entry, n_versions, n_executions] = await Promise.all([
|
||||
getEntry(opts.input),
|
||||
getEntryNVersions(opts.input),
|
||||
getEntryNExecutions(opts.input),
|
||||
]);
|
||||
return { ...entry, n_versions, n_executions };
|
||||
})),
|
||||
})
|
||||
),
|
||||
list: t.procedure
|
||||
.input(ZEntriesQuery)
|
||||
.query(async (opts) => trpcWrap(async () => getEntries(
|
||||
.query(async (opts) =>
|
||||
trpcWrap(async () =>
|
||||
getEntries(
|
||||
opts.input.filter ?? {},
|
||||
opts.input.pagination ?? {},
|
||||
opts.input.sort ?? {},
|
||||
))),
|
||||
opts.input.sort ?? {}
|
||||
)
|
||||
)
|
||||
),
|
||||
versions: t.procedure
|
||||
.input(ZEntityId)
|
||||
.query(async (opts) => trpcWrap(async () => getEntryVersions(opts.input))),
|
||||
versionsDiff: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
||||
versionsDiff: t.procedure.input(ZEntityId).query(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
const versions = await getEntryVersions(opts.input);
|
||||
return versionsDiff(versions);
|
||||
})),
|
||||
})
|
||||
),
|
||||
executions: t.procedure
|
||||
.input(ZEntityId)
|
||||
.query(async (opts) => trpcWrap(async () => getEntryExecutions(opts.input))),
|
||||
executionsDiff: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
||||
executionsDiff: t.procedure.input(ZEntityId).query(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
const executions = await getEntryExecutions(opts.input);
|
||||
return executionsDiff(executions);
|
||||
})),
|
||||
})
|
||||
),
|
||||
create: t.procedure
|
||||
.input(ZEntryNew)
|
||||
.mutation(async (opts) => trpcWrap(async () => newEntry(opts.ctx.user.id, opts.input))),
|
||||
.mutation(async (opts) =>
|
||||
trpcWrap(async () => newEntry(opts.ctx.user.id, opts.input))
|
||||
),
|
||||
newVersion: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
id: ZEntityId,
|
||||
version: ZEntryVersionNew,
|
||||
old_version_id: ZEntityId.optional(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async (opts) => trpcWrap(async () => newEntryVersion(
|
||||
.mutation(async (opts) =>
|
||||
trpcWrap(async () =>
|
||||
newEntryVersion(
|
||||
opts.ctx.user.id,
|
||||
opts.input.id,
|
||||
opts.input.version,
|
||||
opts.input.old_version_id,
|
||||
))),
|
||||
opts.input.old_version_id
|
||||
)
|
||||
)
|
||||
),
|
||||
newExecution: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
id: ZEntityId,
|
||||
execution: ZEntryExecutionNew,
|
||||
old_execution_id: ZEntityId.nullable().optional(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async (opts) => trpcWrap(async () => newEntryExecution(
|
||||
.mutation(async (opts) =>
|
||||
trpcWrap(async () =>
|
||||
newEntryExecution(
|
||||
opts.ctx.user.id,
|
||||
opts.input.id,
|
||||
opts.input.execution,
|
||||
opts.input.old_execution_id,
|
||||
))),
|
||||
opts.input.old_execution_id
|
||||
)
|
||||
)
|
||||
),
|
||||
});
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { fields, ZPatientNew, ZPatientsQuery } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deletePatient,
|
||||
getPatient,
|
||||
|
@ -12,46 +8,55 @@ import {
|
|||
newPatient,
|
||||
updatePatient,
|
||||
} from "$lib/server/query";
|
||||
|
||||
import { fields, ZPatientNew, ZPatientsQuery } from "$lib/shared/model/validation";
|
||||
import { t, trpcWrap } from "..";
|
||||
import { z } from "zod";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const patientRouter = t.router({
|
||||
getNames: t.procedure.query(async () => trpcWrap(getPatientNames)),
|
||||
get: t.procedure.input(ZEntityId).query(async (opts) => trpcWrap(async () => {
|
||||
get: t.procedure.input(ZEntityId).query(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
const [patient, n_entries] = await Promise.all([
|
||||
getPatient(opts.input),
|
||||
getPatientNEntries(opts.input),
|
||||
]);
|
||||
return { ...patient, n_entries };
|
||||
})),
|
||||
list: t.procedure
|
||||
.input(ZPatientsQuery)
|
||||
.query(async (opts) => getPatients(
|
||||
})
|
||||
),
|
||||
list: t.procedure.input(ZPatientsQuery).query(async (opts) => {
|
||||
return getPatients(
|
||||
opts.input.filter ?? {},
|
||||
opts.input.pagination ?? {},
|
||||
opts.input.sort ?? {},
|
||||
)),
|
||||
opts.input.sort ?? {}
|
||||
);
|
||||
}),
|
||||
create: t.procedure
|
||||
.input(ZPatientNew)
|
||||
.mutation(async (opts) => trpcWrap(async () => newPatient(opts.input))),
|
||||
update: t.procedure
|
||||
.input(z.object({ id: ZEntityId, patient: ZPatientNew.partial() }))
|
||||
.mutation(async (opts) => trpcWrap(async () => {
|
||||
.mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
await updatePatient(opts.input.id, opts.input.patient);
|
||||
})),
|
||||
delete: t.procedure.input(ZEntityId).mutation(async (opts) => trpcWrap(async () => {
|
||||
})
|
||||
),
|
||||
delete: t.procedure.input(ZEntityId).mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
await deletePatient(opts.input);
|
||||
})),
|
||||
})
|
||||
),
|
||||
hide: t.procedure
|
||||
.input(
|
||||
z.object({
|
||||
id: ZEntityId,
|
||||
hidden: z.boolean().default(true),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async (opts) => trpcWrap(async () => {
|
||||
.mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
await hidePatient(opts.input.id, opts.input.hidden);
|
||||
})),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { deleteRoom, getRoom, getRooms, updateRoom } from "$lib/server/query";
|
||||
import { fields, ZRoomNew } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deleteRoom, getRoom, getRooms, newRoom, updateRoom,
|
||||
} from "$lib/server/query";
|
||||
|
||||
import { t, trpcWrap } from "..";
|
||||
import { z } from "zod";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
|
@ -15,16 +10,16 @@ export const roomRouter = t.router({
|
|||
get: t.procedure
|
||||
.input(ZEntityId)
|
||||
.query(async (opts) => trpcWrap(async () => getRoom(opts.input))),
|
||||
create: t.procedure.input(ZRoomNew).mutation(async (opts) => trpcWrap(async () => {
|
||||
const id = await newRoom(opts.input);
|
||||
return id;
|
||||
})),
|
||||
update: t.procedure
|
||||
.input(z.object({ id: ZEntityId, room: ZRoomNew.partial() }))
|
||||
.mutation(async (opts) => trpcWrap(async () => {
|
||||
await updateRoom(opts.input.id, opts.input.room);
|
||||
})),
|
||||
delete: t.procedure.input(ZEntityId).mutation(async (opts) => trpcWrap(async () => {
|
||||
.input(z.object({ id: ZEntityId, category: ZRoomNew.partial() }))
|
||||
.mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
await updateRoom(opts.input.id, opts.input.category);
|
||||
})
|
||||
),
|
||||
delete: t.procedure.input(ZEntityId).mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
await deleteRoom(opts.input);
|
||||
})),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { fields, ZStationNew } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deleteStation,
|
||||
getStation,
|
||||
getStations,
|
||||
newStation,
|
||||
updateStation,
|
||||
} from "$lib/server/query";
|
||||
|
||||
import { fields, ZStationNew } from "$lib/shared/model/validation";
|
||||
import { t, trpcWrap } from "..";
|
||||
import { z } from "zod";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
|
@ -19,16 +15,16 @@ export const stationRouter = t.router({
|
|||
get: t.procedure
|
||||
.input(ZEntityId)
|
||||
.query(async (opts) => trpcWrap(async () => getStation(opts.input))),
|
||||
create: t.procedure.input(ZStationNew).mutation(async (opts) => trpcWrap(async () => {
|
||||
const id = await newStation(opts.input);
|
||||
return id;
|
||||
})),
|
||||
update: t.procedure
|
||||
.input(z.object({ id: ZEntityId, station: ZStationNew.partial() }))
|
||||
.mutation(async (opts) => trpcWrap(async () => {
|
||||
await updateStation(opts.input.id, opts.input.station);
|
||||
})),
|
||||
delete: t.procedure.input(ZEntityId).mutation(async (opts) => trpcWrap(async () => {
|
||||
.input(z.object({ id: ZEntityId, category: ZStationNew.partial() }))
|
||||
.mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
await updateStation(opts.input.id, opts.input.category);
|
||||
})
|
||||
),
|
||||
delete: t.procedure.input(ZEntityId).mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
await deleteStation(opts.input);
|
||||
})),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { fields, ZPagination } from "$lib/shared/model/validation";
|
||||
|
||||
import { getUser, getUserNames, getUsers } from "$lib/server/query";
|
||||
|
||||
import { fields, ZPagination } from "$lib/shared/model/validation";
|
||||
import { t, trpcWrap } from "..";
|
||||
import { z } from "zod";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const userRouter = t.router({
|
||||
list: t.procedure
|
||||
.input(z.object({ pagination: ZPagination }).partial())
|
||||
.query(async (opts) => getUsers(opts.input.pagination ?? {})),
|
||||
.query(async (opts) => {
|
||||
return getUsers(opts.input.pagination ?? {});
|
||||
}),
|
||||
getNames: t.procedure.query(getUserNames),
|
||||
get: t.procedure
|
||||
.input(ZEntityId)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, test } from "vitest";
|
||||
|
||||
import { fields } from "./validation";
|
||||
|
||||
test("date string", () => {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { implement } from "$lib/shared/util/zod";
|
||||
|
||||
import type {
|
||||
CategoryNew,
|
||||
EntryExecutionNew,
|
||||
|
@ -11,36 +9,36 @@ import type {
|
|||
PaginationRequest,
|
||||
PatientNew,
|
||||
RoomNew,
|
||||
SortRequest,
|
||||
StationNew,
|
||||
User,
|
||||
} from ".";
|
||||
|
||||
// Fields
|
||||
export const fields = {
|
||||
EntityId: () => z.number().int().min(1),
|
||||
EntityId: () => z.number().int().nonnegative(),
|
||||
NameString: () => z.string().min(1).max(200).trim(),
|
||||
TextString: () => z.string().trim(),
|
||||
Age: () => z.number().int().nonnegative().lt(200),
|
||||
DateString: () => z
|
||||
DateString: () =>
|
||||
z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||
// @ts-expect-error check date for NaN is valid
|
||||
.refine((val) => !isNaN(new Date(val))),
|
||||
DateStringFuture: () => fields.DateString().refine(
|
||||
DateStringFuture: () =>
|
||||
fields.DateString().refine(
|
||||
(d) => {
|
||||
const inp = new Date(d);
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
return inp >= today;
|
||||
},
|
||||
{ message: "Datum muss in der Zukunft liegen" },
|
||||
{ message: "Datum muss in der Zukunft liegen" }
|
||||
),
|
||||
};
|
||||
|
||||
const coercedUint = z.coerce.number().int().nonnegative();
|
||||
const coercedBool = z.string().toLowerCase().transform((v) => v === "true").or(z.boolean());
|
||||
|
||||
export const ZUrlEntityId = coercedUint;
|
||||
export const ZUrlEntityId = z.coerce.number().int().nonnegative();
|
||||
|
||||
export const ZUser = implement<User>().with({
|
||||
id: z.coerce.number().int().nonnegative(),
|
||||
|
@ -60,7 +58,7 @@ export const ZCategoryNew = implement<CategoryNew>().with({
|
|||
description: fields.TextString().nullable(),
|
||||
color: z
|
||||
.string()
|
||||
.regex(/^#[0-9A-Fa-f]{6}$/)
|
||||
.regex(/[0-9A-Fa-f]{6}/)
|
||||
.toUpperCase()
|
||||
.nullable(),
|
||||
});
|
||||
|
@ -94,36 +92,31 @@ export const ZEntryExecutionNew = implement<EntryExecutionNew>().with({
|
|||
});
|
||||
|
||||
export const ZPagination = implement<PaginationRequest>().with({
|
||||
limit: coercedUint.optional(),
|
||||
offset: coercedUint.optional(),
|
||||
limit: fields.EntityId().optional(),
|
||||
offset: fields.EntityId().optional(),
|
||||
});
|
||||
|
||||
export const ZSort = z.object({
|
||||
export const ZSort = implement<SortRequest>().with({
|
||||
field: z.string().optional(),
|
||||
asc: coercedBool.optional(),
|
||||
asc: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const ZFilterListEntry = z.object({
|
||||
id: coercedUint,
|
||||
id: fields.EntityId(),
|
||||
name: fields.NameString().optional(),
|
||||
});
|
||||
const ZFilterList = z.array(ZFilterListEntry);
|
||||
const paginatedQuery = <T extends z.ZodTypeAny>(f: T) => z
|
||||
.object({
|
||||
filter: f,
|
||||
pagination: ZPagination,
|
||||
sort: ZSort,
|
||||
})
|
||||
.partial();
|
||||
const paginatedQuery = (f: z.ZodTypeAny) =>
|
||||
z.object({ filter: f, pagination: ZPagination, sort: ZSort }).partial();
|
||||
|
||||
export const ZEntriesFilter = z
|
||||
.object({
|
||||
author: ZFilterList,
|
||||
category: ZFilterList,
|
||||
done: coercedBool,
|
||||
done: z.boolean(),
|
||||
executor: ZFilterList,
|
||||
patient: ZFilterList,
|
||||
priority: coercedBool,
|
||||
priority: z.boolean(),
|
||||
room: ZFilterList,
|
||||
search: z.string(),
|
||||
station: ZFilterList,
|
||||
|
@ -137,8 +130,8 @@ export const ZPatientsFilter = z
|
|||
search: z.string(),
|
||||
room: ZFilterList,
|
||||
station: ZFilterList,
|
||||
hidden: coercedBool,
|
||||
includeHidden: z.coerce.boolean(),
|
||||
hidden: z.boolean(),
|
||||
includeHidden: z.boolean(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||
import { createTRPCClient, type TRPCClientInit } from "trpc-sveltekit";
|
||||
|
||||
import type { Router } from "$lib/server/trpc/router";
|
||||
import { createTRPCClient, type TRPCClientInit } from "trpc-sveltekit";
|
||||
|
||||
export type RouterInput = inferRouterInputs<Router>;
|
||||
export type RouterOutput = inferRouterOutputs<Router>;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { hexToColor, colorToHex } from "./colors";
|
||||
|
||||
describe.each([
|
||||
|
|
|
@ -13,7 +13,7 @@ const WHITE: Color = { r: 255, g: 255, b: 255 };
|
|||
const BLACK: Color = { r: 0, g: 0, b: 0 };
|
||||
|
||||
export function colorToHex(c: Color): string {
|
||||
return `#${((1 << 24) | (c.r << 16) | (c.g << 8) | c.b).toString(16).slice(1)}`;
|
||||
return "#" + ((1 << 24) | (c.r << 16) | (c.g << 8) | c.b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
export function hexToColor(s: string): Color {
|
||||
|
@ -46,9 +46,10 @@ function convert_8bit_RGB_to_standard_RGB(primaryColor_8bit: number): number {
|
|||
}
|
||||
|
||||
function convert_standard_RGB_to_linear_RGB(primaryColor_sRGB: number): number {
|
||||
const primaryColor_linear = primaryColor_sRGB < 0.03928
|
||||
const primaryColor_linear =
|
||||
primaryColor_sRGB < 0.03928
|
||||
? primaryColor_sRGB / 12.92
|
||||
: ((primaryColor_sRGB + 0.055) / 1.055) ** 2.4;
|
||||
: Math.pow((primaryColor_sRGB + 0.055) / 1.055, 2.4);
|
||||
return primaryColor_linear;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { test, expect } from "vitest";
|
||||
|
||||
import type { EntryVersion } from "$lib/shared/model";
|
||||
|
||||
import { CATEGORIES, U1, U2 } from "$tests/helpers/testdata";
|
||||
|
||||
import { versionsDiff } from "./diff";
|
||||
|
||||
test("versions diff", () => {
|
||||
|
@ -57,6 +54,9 @@ test("versions diff", () => {
|
|||
priority: true,
|
||||
text: [
|
||||
{
|
||||
count: 38,
|
||||
added: true,
|
||||
removed: undefined,
|
||||
value:
|
||||
"10ml Blut abnehmen.\n\nDas Blut muss auf Lambda-Erreger getestet werden.\n\nHierfür ist das Labor Meier zuständig.",
|
||||
},
|
||||
|
@ -75,18 +75,8 @@ test("versions diff", () => {
|
|||
priority: undefined,
|
||||
text: [
|
||||
{ count: 16, value: "10ml Blut abnehmen.\n\nDas Blut muss auf " },
|
||||
{
|
||||
count: 1,
|
||||
added: undefined,
|
||||
removed: true,
|
||||
value: "Lambda",
|
||||
},
|
||||
{
|
||||
count: 1,
|
||||
added: true,
|
||||
removed: undefined,
|
||||
value: "XYZ",
|
||||
},
|
||||
{ count: 1, added: undefined, removed: true, value: "Lambda" },
|
||||
{ count: 1, added: true, removed: undefined, value: "XYZ" },
|
||||
{
|
||||
count: 21,
|
||||
value: "-Erreger getestet werden.\n\nHierfür ist das Labor Meier zuständig.",
|
||||
|
@ -104,12 +94,7 @@ test("versions diff", () => {
|
|||
category: undefined,
|
||||
text: [
|
||||
{ count: 17, value: "10ml Blut abnehmen.\n\nDas Blut muss auf XYZ" },
|
||||
{
|
||||
count: 2,
|
||||
added: undefined,
|
||||
removed: true,
|
||||
value: "-Erreger",
|
||||
},
|
||||
{ count: 2, added: undefined, removed: true, value: "-Erreger" },
|
||||
{ count: 5, value: " getestet werden." },
|
||||
{
|
||||
count: 14,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { diffWords } from "diff";
|
||||
|
||||
import type {
|
||||
EntryVersion,
|
||||
EntryExecution,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export class ErrorConflict extends Error {
|
||||
constructor(msg: string) {
|
||||
super(`conflict: ${msg}`);
|
||||
super("conflict: " + msg);
|
||||
Object.setPrototypeOf(this, ErrorConflict.prototype);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { expect, it, vi } from "vitest";
|
||||
|
||||
import type { EntityQuery } from "$lib/shared/model";
|
||||
import { ZEntriesQuery } from "$lib/shared/model/validation";
|
||||
|
||||
import { getQueryUrl, humanDate, parseQueryUrl } from ".";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { humanDate } from ".";
|
||||
|
||||
const MINUTE = 60000;
|
||||
const HOUR = 3_600_000;
|
||||
const DAY = 24 * HOUR;
|
||||
|
||||
it.each([
|
||||
test.each([
|
||||
{ s: 0, txt: "jetzt gerade" },
|
||||
{ s: -30 * MINUTE, txt: "vor 30 Minuten" },
|
||||
{ s: -11 * HOUR, txt: "vor 11 Stunden" },
|
||||
|
@ -34,22 +30,3 @@ it.each([
|
|||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("getQueryUrl", () => {
|
||||
const query: EntityQuery = {
|
||||
filter: {
|
||||
author: [{ id: 2, name: "Max" }],
|
||||
category: [{ id: 1, name: "Blutabnahme" }, { id: 2, name: "Labortests" }],
|
||||
done: true,
|
||||
search: "Hello World",
|
||||
},
|
||||
pagination: { limit: 10, offset: 20 },
|
||||
sort: { field: "room", asc: true },
|
||||
};
|
||||
|
||||
const queryUrl = getQueryUrl(query, "");
|
||||
expect(queryUrl).toBe("?filter%5Bauthor%5D%5B0%5D%5Bid%5D=2&filter%5Bauthor%5D%5B0%5D%5Bname%5D=Max&filter%5Bcategory%5D%5B0%5D%5Bid%5D=1&filter%5Bcategory%5D%5B0%5D%5Bname%5D=Blutabnahme&filter%5Bcategory%5D%5B1%5D%5Bid%5D=2&filter%5Bcategory%5D%5B1%5D%5Bname%5D=Labortests&filter%5Bdone%5D=true&filter%5Bsearch%5D=Hello%20World&pagination%5Blimit%5D=10&pagination%5Boffset%5D=20&sort%5Bfield%5D=room&sort%5Basc%5D=true");
|
||||
|
||||
const decoded = ZEntriesQuery.parse(parseQueryUrl(queryUrl));
|
||||
expect(decoded).toStrictEqual(query);
|
||||
});
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import { goto } from "$app/navigation";
|
||||
|
||||
import { isRedirect, error } from "@sveltejs/kit";
|
||||
import { TRPCClientError } from "@trpc/client";
|
||||
import qs from "qs";
|
||||
import { ZodError } from "zod";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
import type { EntityQuery } from "$lib/shared/model";
|
||||
import { TRPCClientError } from "@trpc/client";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
const LOCALE = "de-DE";
|
||||
|
||||
|
@ -27,13 +23,14 @@ export function formatDate(date: Date | string, time = false): string {
|
|||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return dt.toLocaleDateString(LOCALE, {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const MS_PER_DAY = 86400000;
|
||||
|
||||
|
@ -49,7 +46,7 @@ export function humanDate(date: Date | string, time = false): string {
|
|||
const dt = coerceDate(date);
|
||||
const threshold = 302400000; // 3.5 * 24 * 3_600_000
|
||||
const diff = Number(dt) - Number(now); // pos: Future, neg: Past
|
||||
if (Math.abs(diff) > threshold) return `am ${formatDate(date, time)}`;
|
||||
if (Math.abs(diff) > threshold) return "am " + formatDate(date, time);
|
||||
|
||||
const intl = new Intl.RelativeTimeFormat(LOCALE);
|
||||
|
||||
|
@ -75,30 +72,25 @@ export function formatBool(val: boolean): string {
|
|||
return val ? "Ja" : "Nein";
|
||||
}
|
||||
|
||||
/** Encode an entity query (search and filter) into an URL */
|
||||
export function getQueryUrl(query: EntityQuery, basePath: string): string {
|
||||
if (Object.values(query).filter((q) => q !== undefined).length === 0) return basePath;
|
||||
return basePath + "?" + qs.stringify(query);
|
||||
export function getQueryUrl(q: EntityQuery, basePath: string): string {
|
||||
if (Object.values(q).filter((q) => q !== undefined).length === 0) return basePath;
|
||||
return encodeURI(basePath + "/" + encodeURIComponent(JSON.stringify(q)));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function parseQueryUrl(search: string): any {
|
||||
return qs.parse(search, { ignoreQueryPrefix: true, allowDots: true });
|
||||
}
|
||||
|
||||
export function gotoEntityQuery(query: EntityQuery, basePath: string) {
|
||||
if (window && window.location.pathname.startsWith(`${basePath}/`)) {
|
||||
if (window.location.search) {
|
||||
const oldQuery: EntityQuery = parseQueryUrl(window.location.search);
|
||||
const newQuery: EntityQuery = {
|
||||
filter: { ...oldQuery.filter, ...query.filter },
|
||||
sort: query.sort,
|
||||
export function gotoEntityQuery(q: EntityQuery, basePath: string) {
|
||||
if (window && window.location.pathname.startsWith(basePath + "/")) {
|
||||
const qstr = decodeURI(window.location.pathname.substring(basePath.length + 1));
|
||||
if (qstr) {
|
||||
const oldq: EntityQuery = JSON.parse(qstr);
|
||||
const nq: EntityQuery = {
|
||||
filter: { ...oldq.filter, ...q.filter },
|
||||
sort: q.sort,
|
||||
};
|
||||
goto(getQueryUrl(newQuery, basePath));
|
||||
goto(getQueryUrl(nq, basePath));
|
||||
return;
|
||||
}
|
||||
}
|
||||
goto(getQueryUrl(query, basePath));
|
||||
goto(getQueryUrl(q, basePath));
|
||||
}
|
||||
|
||||
/** Wrap a page load query to handle occuring errors
|
||||
|
@ -109,9 +101,7 @@ export async function loadWrap<T>(f: () => Promise<T>) {
|
|||
try {
|
||||
return await f();
|
||||
} catch (e) {
|
||||
if (isRedirect(e)) {
|
||||
throw e;
|
||||
} else if (e instanceof TRPCClientError) {
|
||||
if (e instanceof TRPCClientError) {
|
||||
error(e.data?.httpStatus ?? 500, e.message);
|
||||
} else if (e instanceof ZodError) {
|
||||
error(400, e.message);
|
||||
|
@ -124,7 +114,7 @@ export async function loadWrap<T>(f: () => Promise<T>) {
|
|||
}
|
||||
|
||||
export function baseUrl(url: URL): string {
|
||||
return `${url.protocol}//${url.host}`;
|
||||
return url.protocol + "//" + url.host;
|
||||
}
|
||||
|
||||
export function dateToYMD(date: Date): string {
|
||||
|
@ -133,9 +123,7 @@ export function dateToYMD(date: Date): string {
|
|||
|
||||
export class Debouncer {
|
||||
private delay: number;
|
||||
|
||||
private handler: () => unknown;
|
||||
|
||||
private timeout: number | null = null;
|
||||
|
||||
constructor(delay: number, handler: () => unknown) {
|
||||
|
@ -157,7 +145,3 @@ export class Debouncer {
|
|||
this.handler();
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeHtml(s: string): string {
|
||||
return DOMPurify.sanitize(s, { FORBID_TAGS: ["img"] });
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export function implement<Model = never>() {
|
|||
[unknownKey in Exclude<keyof Schema, keyof Model>]: never;
|
||||
},
|
||||
>(
|
||||
schema: Schema,
|
||||
schema: Schema
|
||||
) => z.object(schema),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
|
||||
import NavLink from "$lib/components/ui/NavLink.svelte";
|
||||
import { mdiAccount, mdiHome } from "@mdi/js";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import NavLink from "$lib/components/ui/NavLink.svelte";
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -13,41 +12,35 @@
|
|||
>
|
||||
<nav class="navbar w-full min-h-12">
|
||||
<div class="flex flex-1">
|
||||
<NavLink
|
||||
active={$page.route.id === "/(app)"}
|
||||
href="/"
|
||||
<NavLink active={$page.route.id === "/(app)"} href="/"
|
||||
><Icon
|
||||
cls={$page.route.id === "/(app)" ? "text-primary" : ""}
|
||||
path={mdiHome}
|
||||
cls={$page.route.id === "/(app)" ? "text-primary" : ""}
|
||||
/></NavLink
|
||||
>
|
||||
<NavLink
|
||||
active={$page.route.id === "/(app)/plan"}
|
||||
href="/plan"
|
||||
<NavLink active={$page.route.id === "/(app)/plan/[[query]]"} href="/plan"
|
||||
>Planung</NavLink
|
||||
>
|
||||
<NavLink active={$page.route.id === "/(app)/visit"} href="/visit">Visite</NavLink>
|
||||
<NavLink
|
||||
active={$page.route.id === "/(app)/patients"}
|
||||
href="/patients"
|
||||
<NavLink active={$page.route.id === "/(app)/patients/[[query]]"} href="/patients"
|
||||
>Patienten</NavLink
|
||||
>
|
||||
</div>
|
||||
<div class="flex-0">
|
||||
{#if $page.data.session?.user}
|
||||
<div class="dropdown dropdown-hover dropdown-end">
|
||||
<div class="btn btn-sm btn-ghost" role="button" tabindex="0">
|
||||
<div tabindex="0" role="button" class="btn btn-sm btn-ghost">
|
||||
<Icon path={mdiAccount} />
|
||||
<span class="hidden md:inline">{$page.data.session.user?.name}</span>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
|
||||
tabindex="0"
|
||||
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
|
||||
>
|
||||
<li><a href="/stations">Stationen</a></li>
|
||||
<li><a href="/rooms">Zimmer</a></li>
|
||||
<li><a href="/categories">Kategorien</a></li>
|
||||
<li><a href="/users">Benutzer</a></li>
|
||||
<li><a href="/logout">Abmelden</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script lang="ts">
|
||||
<script>
|
||||
import { page } from "$app/stores";
|
||||
</script>
|
||||
|
||||
|
@ -12,14 +12,15 @@
|
|||
<p>Sie sind nicht angemeldet</p>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-flow-row gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
<div
|
||||
class="grid grid-flow-row gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Planung</h2>
|
||||
<p>Hier können sie neue Visitenbucheinträge erstellen.</p>
|
||||
<div class="card-actions justify-end">
|
||||
<a class="btn btn-primary" href="/plan">Planung</a>
|
||||
<a href="/plan" class="btn btn-primary">Planung</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,7 +31,7 @@
|
|||
<p>Hier können sie Visitenbucheinträge abarbeiten.</p>
|
||||
<p>Heute müssen (n) Einträge erledigt werden.</p>
|
||||
<div class="card-actions justify-end">
|
||||
<a class="btn btn-primary" href="/visit">Visite</a>
|
||||
<a href="/visit" class="btn btn-primary">Visite</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Kategorien</title>
|
||||
</svelte:head>
|
||||
|
||||
<Header title="Kategorien">
|
||||
<a slot="rightBtn" class="btn btn-sm btn-primary ml-auto" href="/category/new">Neue Kategorie</a>
|
||||
</Header>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Beschreibung</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.categories as category (category.id)}
|
||||
<tr>
|
||||
<td>
|
||||
<CategoryField {category} href="/category/{category.id}" />
|
||||
</td>
|
||||
<td>
|
||||
{#if category.description}
|
||||
{category.description}
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||
import type { PageLoad } from "./$types";
|
||||
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
return loadWrap(async () => {
|
||||
const categories = await trpc(event).category.list.query();
|
||||
return { categories };
|
||||
});
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
import type { Actions } from "./$types";
|
||||
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import { ZCategoryNew, ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => loadWrap(async () => {
|
||||
const id = ZUrlEntityId.parse(event.params.id);
|
||||
const formData = await event.request.formData();
|
||||
|
||||
const del = formData.get("delete");
|
||||
if (del) {
|
||||
await trpc(event).category.delete.mutate(id);
|
||||
redirect(302, "/categories");
|
||||
} else {
|
||||
const form = await superValidate(formData, zod(ZCategoryNew));
|
||||
|
||||
if (!form.valid) {
|
||||
return fail(400, { form });
|
||||
}
|
||||
|
||||
await trpc(event).category.update.mutate({
|
||||
id,
|
||||
category: form.data,
|
||||
});
|
||||
|
||||
return { form };
|
||||
}
|
||||
}),
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import CategoryForm from "$lib/components/form/CategoryForm.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Kategorie: {data.category.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<CategoryForm category={data.category} formData={data.form}>
|
||||
<button name="delete" class="btn btn-error" type="submit" value="1">Löschen</button>
|
||||
</CategoryForm>
|
|
@ -1,18 +0,0 @@
|
|||
import type { PageLoad } from "./$types";
|
||||
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import { ZCategoryNew, ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
return loadWrap(async () => {
|
||||
const id = ZUrlEntityId.parse(event.params.id);
|
||||
const category = await trpc(event).category.get.query(id);
|
||||
const form = await superValidate(category, zod(ZCategoryNew));
|
||||
|
||||
return { category, form };
|
||||
});
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
import type { Actions } from "./$types";
|
||||
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import { ZCategoryNew } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event.request, zod(ZCategoryNew));
|
||||
|
||||
if (!form.valid) {
|
||||
return fail(400, { form });
|
||||
}
|
||||
|
||||
const newId = await loadWrap(async () => trpc(event).category.create.mutate(form.data));
|
||||
throw redirect(302, `/category/${newId}`);
|
||||
},
|
||||
};
|
|
@ -1,6 +0,0 @@
|
|||
<script lang="ts">
|
||||
import CategoryForm from "$lib/components/form/CategoryForm.svelte";
|
||||
|
||||
</script>
|
||||
|
||||
<CategoryForm />
|
|
@ -1,26 +1,25 @@
|
|||
import type { Actions } from "./$types";
|
||||
|
||||
import { fail } from "@sveltejs/kit";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
import type { Actions } from "./$types";
|
||||
import { SchemaEntryExecution } from "./schema";
|
||||
import { fail } from "@sveltejs/kit";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => loadWrap(async () => {
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event.request, SchemaEntryExecution);
|
||||
if (!form.valid) {
|
||||
return fail(400, { form });
|
||||
}
|
||||
|
||||
await loadWrap(async () => {
|
||||
const id = ZUrlEntityId.parse(event.params.id);
|
||||
await trpc(event).entry.newExecution.mutate({
|
||||
id,
|
||||
old_execution_id: null,
|
||||
execution: { text: form.data.text },
|
||||
});
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
|
||||
import EntryBody from "$lib/components/entry/EntryBody.svelte";
|
||||
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
||||
|
||||
import MarkdownInput from "$lib/components/ui/MarkdownInput.svelte";
|
||||
import { SchemaEntryExecution } from "./schema";
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import EntryBody from "$lib/components/entry/EntryBody.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const formData = defaults(SchemaEntryExecution);
|
||||
let formData = defaults(SchemaEntryExecution);
|
||||
const { form, errors, enhance } = superForm(formData, {
|
||||
validators: SchemaEntryExecution,
|
||||
});
|
||||
|
@ -22,14 +18,14 @@
|
|||
{#if !data.entry.execution}
|
||||
<form method="POST" use:enhance>
|
||||
<MarkdownInput
|
||||
label="Eintrag erledigen"
|
||||
name="text"
|
||||
ariaInvalid={Boolean($errors.text)}
|
||||
errors={$errors.text}
|
||||
label="Eintrag erledigen"
|
||||
bind:value={$form.text}
|
||||
errors={$errors.text}
|
||||
>
|
||||
<div class="row c-vlight">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Erledigt</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Erledigt</button>
|
||||
</div>
|
||||
</MarkdownInput>
|
||||
</form>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import type { PageLoad } from "./$types";
|
||||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
const entry = await loadWrap(async () => {
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
import type { Actions } from "./$types";
|
||||
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
import type { Actions } from "./$types";
|
||||
import { SchemaNewEntryVersion } from "./schema";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => loadWrap(async () => {
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event.request, SchemaNewEntryVersion);
|
||||
if (!form.valid) {
|
||||
return fail(400, { form });
|
||||
}
|
||||
|
||||
const entryId = await loadWrap(async () => {
|
||||
const id = ZUrlEntityId.parse(event.params.id);
|
||||
await trpc(event).entry.newVersion.mutate({
|
||||
id,
|
||||
version: form.data,
|
||||
old_version_id: form.data.old_version_id,
|
||||
});
|
||||
return id;
|
||||
});
|
||||
|
||||
redirect(302, `/entry/${id}`);
|
||||
}),
|
||||
};
|
||||
throw redirect(302, `/entry/${entryId}`);
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
|
|
@ -1,28 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { formatDate, humanDate } from "$lib/shared/util";
|
||||
|
||||
import PatientCard from "$lib/components/entry/PatientCard.svelte";
|
||||
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
||||
import UserField from "$lib/components/table/UserField.svelte";
|
||||
import FormField from "$lib/components/ui/FormField.svelte";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
||||
|
||||
import PatientCard from "$lib/components/entry/PatientCard.svelte";
|
||||
import { formatDate, humanDate } from "$lib/shared/util";
|
||||
import FormField from "$lib/components/ui/FormField.svelte";
|
||||
import Autocomplete from "$lib/components/filter/Autocomplete.svelte";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { SchemaNewEntryVersion } from "./schema";
|
||||
import MarkdownInput from "$lib/components/ui/MarkdownInput.svelte";
|
||||
import UserField from "$lib/components/table/UserField.svelte";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: basePath = `/entry/${data.entry.id}`;
|
||||
|
||||
const {
|
||||
form, errors, constraints, enhance, tainted,
|
||||
} = superForm(data.form, {
|
||||
const { form, errors, constraints, enhance, tainted } = superForm(data.form, {
|
||||
validators: SchemaNewEntryVersion,
|
||||
});
|
||||
</script>
|
||||
|
@ -31,28 +26,30 @@
|
|||
<title>Eintrag #{data.entry.id}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Header backHref={basePath} title="Eintrag #{data.entry.id} bearbeiten" />
|
||||
<Header title="Eintrag #{data.entry.id} bearbeiten" backHref={basePath}></Header>
|
||||
|
||||
<p class="text-sm flex flex-row gap-2">
|
||||
<span>Erstellt {humanDate(data.entry.created_at, true)}</span>
|
||||
<span>·</span>
|
||||
<span>
|
||||
Zuletzt bearbeitet am {formatDate(data.entry.current_version.created_at, true)} von
|
||||
<UserField filterName="author" user={data.entry.current_version.author} />
|
||||
<UserField user={data.entry.current_version.author} filterName="author" />
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<PatientCard patient={data.entry.patient} />
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<input name="old_version_id" type="hidden" value={$form.old_version_id} />
|
||||
<input type="hidden" name="old_version_id" value={$form.old_version_id} />
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<FormField errors={$errors.category_id} label="Kategorie">
|
||||
<FormField label="Kategorie" errors={$errors.category_id}>
|
||||
<Autocomplete
|
||||
idInputName="category_id"
|
||||
inputCls="input input-bordered w-full max-w-xs"
|
||||
items={async () => trpc().category.list.query()}
|
||||
items={async () => {
|
||||
return await trpc().category.list.query();
|
||||
}}
|
||||
selection={data.entry.current_version.category}
|
||||
onSelect={(item) => {
|
||||
$form.category_id = item.id;
|
||||
return { newValue: item.name ?? "", close: true };
|
||||
|
@ -60,15 +57,15 @@
|
|||
onUnselect={() => {
|
||||
$form.category_id = null;
|
||||
}}
|
||||
selection={data.entry.current_version.category}
|
||||
idInputName="category_id"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField errors={$errors.date} label="Zu erledigen am">
|
||||
<FormField label="Zu erledigen am" errors={$errors.date}>
|
||||
<input
|
||||
type="date"
|
||||
name="date"
|
||||
aria-invalid={Boolean($errors.date)}
|
||||
type="date"
|
||||
bind:value={$form.date}
|
||||
{...$constraints.date}
|
||||
/>
|
||||
|
@ -78,9 +75,9 @@
|
|||
<label class="label cursor-pointer gap-2 justify-start">
|
||||
<span class="label-text text-right">Priorität</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="priority"
|
||||
class="checkbox checkbox-warning"
|
||||
type="checkbox"
|
||||
bind:checked={$form.priority}
|
||||
/>
|
||||
</label>
|
||||
|
@ -88,17 +85,17 @@
|
|||
</div>
|
||||
|
||||
<MarkdownInput
|
||||
name="text"
|
||||
ariaInvalid={Boolean($errors.text)}
|
||||
errors={$errors.text}
|
||||
label="Beschreibung"
|
||||
name="text"
|
||||
marginTop
|
||||
ariaInvalid={Boolean($errors.text)}
|
||||
bind:value={$form.text}
|
||||
errors={$errors.text}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="btn btn-primary max-w-32 mt-4"
|
||||
disabled={browser && $tainted === undefined}
|
||||
type="submit">Speichern</button
|
||||
type="submit"
|
||||
disabled={browser && $tainted === undefined}>Speichern</button
|
||||
>
|
||||
</form>
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import type { PageLoad } from "./$types";
|
||||
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import type { PageLoad } from "./$types";
|
||||
import { SchemaNewEntryVersion } from "./schema";
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
|
@ -19,7 +16,7 @@ export const load: PageLoad = async (event) => {
|
|||
old_version_id: entry.current_version.id,
|
||||
...entry.current_version,
|
||||
},
|
||||
SchemaNewEntryVersion,
|
||||
SchemaNewEntryVersion
|
||||
);
|
||||
|
||||
return { entry, form };
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { ZEntryVersionNew, fields } from "$lib/shared/model/validation";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ZEntryVersionNew, fields } from "$lib/shared/model/validation";
|
||||
|
||||
export const SchemaNewEntryVersion = zod(
|
||||
ZEntryVersionNew.extend({
|
||||
old_version_id: fields.EntityId(),
|
||||
// Override priority field so checkbox value is always true/false, not optional
|
||||
priority: z.boolean(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
import type { Actions } from "./$types";
|
||||
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
import type { Actions } from "./$types";
|
||||
import { SchemaNewExecution } from "./schema";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => loadWrap(async () => {
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event.request, SchemaNewExecution);
|
||||
if (!form.valid) {
|
||||
return fail(400, { form });
|
||||
}
|
||||
|
||||
const entryId = await loadWrap(async () => {
|
||||
const id = ZUrlEntityId.parse(event.params.id);
|
||||
await trpc(event).entry.newExecution.mutate({
|
||||
id,
|
||||
execution: form.data,
|
||||
old_execution_id: form.data.old_execution_id,
|
||||
});
|
||||
return id;
|
||||
});
|
||||
|
||||
redirect(302, `/entry/${id}`);
|
||||
}),
|
||||
};
|
||||
throw redirect(302, `/entry/${entryId}`);
|
||||
},
|
||||
} satisfies Actions;
|
||||
|
|
|
@ -1,34 +1,30 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
|
||||
import EntryBody from "$lib/components/entry/EntryBody.svelte";
|
||||
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
||||
|
||||
import MarkdownInput from "$lib/components/ui/MarkdownInput.svelte";
|
||||
import { SchemaNewExecution } from "./schema";
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import EntryBody from "$lib/components/entry/EntryBody.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const formData = defaults(SchemaNewExecution);
|
||||
let formData = defaults(SchemaNewExecution);
|
||||
const { form, errors, enhance } = superForm(formData, {
|
||||
validators: SchemaNewExecution,
|
||||
});
|
||||
</script>
|
||||
|
||||
<EntryBody backBtn entry={data.entry} />
|
||||
<EntryBody entry={data.entry} backBtn />
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<MarkdownInput
|
||||
label="Ausführungstext bearbeiten"
|
||||
name="text"
|
||||
ariaInvalid={Boolean($errors.text)}
|
||||
errors={$errors.text}
|
||||
label="Ausführungstext bearbeiten"
|
||||
bind:value={$form.text}
|
||||
errors={$errors.text}
|
||||
>
|
||||
<div class="row c-vlight">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Speichern</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Speichern</button>
|
||||
</div>
|
||||
</MarkdownInput>
|
||||
</form>
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import type { PageLoad } from "./$types";
|
||||
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
|
||||
import { ZUrlEntityId } from "$lib/shared/model/validation";
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
import { loadWrap } from "$lib/shared/util";
|
||||
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import type { PageLoad } from "./$types";
|
||||
import { SchemaNewExecution } from "./schema";
|
||||
|
||||
export const load: PageLoad = async (event) => {
|
||||
|
@ -19,7 +16,7 @@ export const load: PageLoad = async (event) => {
|
|||
old_execution_id: entry.execution?.id,
|
||||
...entry.execution,
|
||||
},
|
||||
SchemaNewExecution,
|
||||
SchemaNewExecution
|
||||
);
|
||||
|
||||
return { entry, form };
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import { ZEntryExecutionNew, fields } from "$lib/shared/model/validation";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
export const SchemaNewExecution = zod(
|
||||
ZEntryExecutionNew.extend({
|
||||
old_execution_id: fields.EntityId().optional(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import { formatDate } from "$lib/shared/util";
|
||||
|
||||
import UserField from "$lib/components/table/UserField.svelte";
|
||||
import { formatDate } from "$lib/shared/util";
|
||||
import type { PageData } from "./$types";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
export let data: PageData;
|
||||
$: entryId = $page.params.id;
|
||||
|
@ -15,7 +13,7 @@
|
|||
<title>Eintrag #{entryId} - Erledigt</title>
|
||||
</svelte:head>
|
||||
|
||||
<Header backHref="/entry/{entryId}" title="Eintrag #{entryId} - Erledigt" />
|
||||
<Header title="Eintrag #{entryId} - Erledigt" backHref="/entry/{entryId}" />
|
||||
|
||||
{#each data.versions as version, i}
|
||||
<div class="card2">
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue