Compare commits
11 commits
9a6b8094da
...
453efb0db7
Author | SHA1 | Date | |
---|---|---|---|
453efb0db7 | |||
1c4d04c973 | |||
d7f7b1f539 | |||
e7b6ec0f53 | |||
54b780161e | |||
e1a682747f | |||
63eed6e1fd | |||
ed972b68b3 | |||
28c9a34794 | |||
362829d173 | |||
e8e93a82ae |
151 changed files with 4987 additions and 1554 deletions
|
@ -1,13 +0,0 @@
|
|||
.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
|
|
@ -1,44 +0,0 @@
|
|||
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,11 +7,6 @@ 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
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
.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
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 88,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
834
eslint.config.js
Normal file
834
eslint.config.js
Normal file
|
@ -0,0 +1,834 @@
|
|||
// @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,23 +7,24 @@
|
|||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest --run && vitest --config vitest.config.integration.ts --run",
|
||||
"test": "vitest --run && vitest --config vitest.config.integration.js --run",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"format": "eslint . --fix",
|
||||
"test:unit": "vitest",
|
||||
"test:integration": "vitest --config vitest.config.integration.ts",
|
||||
"test:integration": "vitest --config vitest.config.integration.js",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.28.2",
|
||||
"@auth/core": "^0.30.0",
|
||||
"@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.6.0",
|
||||
"marked": "^12.0.1",
|
||||
"isomorphic-dompurify": "^2.7.0",
|
||||
"qs": "^6.12.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"zod": "^3.22.4",
|
||||
|
@ -31,40 +32,47 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@playwright/test": "^1.42.1",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@stylistic/eslint-plugin": "^1.7.2",
|
||||
"@sveltejs/adapter-node": "^5.0.1",
|
||||
"@sveltejs/kit": "^2.5.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@sveltejs/kit": "^2.5.6",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@trpc/client": "^10.45.2",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@types/diff": "^5.0.9",
|
||||
"@types/node": "^20.12.4",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/qs": "^6.9.15",
|
||||
"@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.9.0",
|
||||
"daisyui": "^4.10.2",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-no-relative-import-paths": "^1.5.3",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"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",
|
||||
"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.12",
|
||||
"svelte": "^4.2.15",
|
||||
"svelte-check": "^3.6.9",
|
||||
"sveltekit-superforms": "^2.12.2",
|
||||
"sveltekit-superforms": "^2.12.5",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"trpc-sveltekit": "^3.6.1",
|
||||
"tslib": "^2.6.2",
|
||||
"tsx": "^4.7.2",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.8",
|
||||
"vitest": "^1.4.0"
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.7.0",
|
||||
"vite": "^5.2.9",
|
||||
"vitest": "^1.5.0"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"carta-md@4.0.0": "patches/carta-md@4.0.0.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
patches/carta-md@4.0.0.patch
Normal file
27
patches/carta-md@4.0.0.patch
Normal file
|
@ -0,0 +1,27 @@
|
|||
# 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,12 +1,10 @@
|
|||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
export default defineConfig({
|
||||
webServer: {
|
||||
command: "npm run build && npm run preview",
|
||||
port: 4173,
|
||||
},
|
||||
testDir: "tests",
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
|
||||
};
|
||||
|
||||
export default config;
|
||||
});
|
2330
pnpm-lock.yaml
2330
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,3 @@
|
|||
// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths
|
||||
import generateMockdata from "../tests/helpers/generate-mockdata"
|
||||
import generateMockdata from "../tests/helpers/generate-mockdata";
|
||||
|
||||
await generateMockdata();
|
||||
|
|
36
src/app.d.ts
vendored
36
src/app.d.ts
vendored
|
@ -20,4 +20,40 @@ 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,6 +3,20 @@
|
|||
@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;
|
||||
}
|
||||
|
@ -17,10 +31,6 @@ button {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.border-1 {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.v-form-field > input {
|
||||
@apply input input-bordered w-full max-w-xs;
|
||||
}
|
||||
|
@ -34,7 +44,7 @@ button {
|
|||
@apply bg-base-200;
|
||||
@apply rounded-xl;
|
||||
@apply flex flex-col;
|
||||
@apply border-solid border-base-content/30 border-[1px];
|
||||
@apply border border-solid border-base-content/30;
|
||||
|
||||
.row {
|
||||
@apply flex flex-row;
|
||||
|
@ -43,7 +53,7 @@ button {
|
|||
.row,
|
||||
.rowb {
|
||||
@apply p-2;
|
||||
@apply border-solid border-base-content/30 border-t-[1px];
|
||||
@apply border-t border-solid border-base-content/30;
|
||||
}
|
||||
|
||||
.row:first-child {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
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
|
||||
|
@ -13,8 +12,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,14 +1,17 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
|
||||
import type { RouterOutput } from "$lib/shared/trpc";
|
||||
import { formatDate, humanDate } from "$lib/shared/util";
|
||||
import UserField from "$lib/components/table/UserField.svelte";
|
||||
|
||||
import PatientCard from "$lib/components/entry/PatientCard.svelte";
|
||||
import CategoryField from "$lib/components/table/CategoryField.svelte";
|
||||
import Markdown from "$lib/components/ui/Markdown.svelte";
|
||||
import UserField from "$lib/components/table/UserField.svelte";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import Markdown from "$lib/components/ui/markdown/Markdown.svelte";
|
||||
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;
|
||||
|
@ -21,14 +24,14 @@
|
|||
<title>Eintrag #{entry.id}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Header title="Eintrag #{entry.id}" backHref={backBtn ? basePath : undefined}>
|
||||
<Header backHref={backBtn ? basePath : undefined} title="Eintrag #{entry.id}">
|
||||
{#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" href="{basePath}/edit" class="btn btn-sm btn-primary ml-auto">
|
||||
<a slot="rightBtn" class="btn btn-sm btn-primary ml-auto" href="{basePath}/edit">
|
||||
Bearbeiten
|
||||
</a>
|
||||
</Header>
|
||||
|
@ -50,19 +53,17 @@
|
|||
Beschreibung
|
||||
<div>
|
||||
<VersionsButton href="{basePath}/versions" n={entry.n_versions} />
|
||||
<a href="{basePath}/edit" class="btn btn-circle btn-sm btn-ghost">
|
||||
<a class="btn btn-circle btn-sm btn-ghost" href="{basePath}/edit">
|
||||
<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 user={entry.current_version.author} filterName="author" />
|
||||
<UserField filterName="author" user={entry.current_version.author} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -71,20 +72,18 @@
|
|||
<div class="row c-light text-sm items-center justify-between">
|
||||
<p>
|
||||
Erledigt am {formatDate(entry.execution.created_at, true)} von
|
||||
<UserField user={entry.execution.author} filterName="executor" />
|
||||
<UserField filterName="executor" user={entry.execution.author} />
|
||||
</p>
|
||||
<div>
|
||||
<VersionsButton href="{basePath}/executions" n={entry.n_executions} />
|
||||
<a href="{basePath}/editExecution" class="btn btn-circle btn-xs btn-ghost">
|
||||
<a class="btn btn-circle btn-xs btn-ghost" href="{basePath}/editExecution">
|
||||
<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,5 +1,6 @@
|
|||
<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,12 +7,15 @@
|
|||
*/
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import { createFloatingActions } from "svelte-floating-ui";
|
||||
import { shift } from "svelte-floating-ui/dom";
|
||||
import outclick from "$lib/actions/outclick";
|
||||
import type { BaseItem } from "./types";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import outclick from "$lib/actions/outclick";
|
||||
|
||||
import type { BaseItem } from "./types";
|
||||
|
||||
type T = $$Generic<BaseItem>;
|
||||
type OnSelectResult = { newValue: string; close: boolean };
|
||||
|
@ -87,9 +90,9 @@
|
|||
return false;
|
||||
} else {
|
||||
isLoading = true;
|
||||
items().then((items) => {
|
||||
srcItems = items;
|
||||
if (cacheKey) cache[cacheKey] = items;
|
||||
items().then((fetchedItems) => {
|
||||
srcItems = fetchedItems;
|
||||
if (cacheKey) cache[cacheKey] = fetchedItems;
|
||||
isLoading = false;
|
||||
updateSearch();
|
||||
});
|
||||
|
@ -134,14 +137,12 @@
|
|||
|
||||
function updateSearch() {
|
||||
if (loadSrcItems()) {
|
||||
let searchWord = inputValue().toLowerCase().trim();
|
||||
filteredItems =
|
||||
!selection && searchWord.length > 0
|
||||
const 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();
|
||||
|
@ -179,7 +180,7 @@
|
|||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
let key = e.key;
|
||||
let { key } = e;
|
||||
if (key === "Tab" && e.shiftKey) key = "ShiftTab";
|
||||
const fnmap: { [key: string]: () => void } = {
|
||||
Tab: () => close,
|
||||
|
@ -277,13 +278,14 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-grow {cls}" use:outclick on:outclick={close}>
|
||||
<div class="flex-grow {cls}" on:outclick={close} use:outclick>
|
||||
<input
|
||||
bind:this={inputElm}
|
||||
class={inputCls}
|
||||
class:px-2={padding}
|
||||
type="text"
|
||||
{placeholder}
|
||||
bind:this={inputElm}
|
||||
type="text"
|
||||
value=""
|
||||
on:input={onInput}
|
||||
on:click={open}
|
||||
on:focus={open}
|
||||
|
@ -291,30 +293,31 @@
|
|||
on:keypress={onKeyPress}
|
||||
on:blur={onBlur}
|
||||
use:floatingRef
|
||||
value=""
|
||||
/>
|
||||
|
||||
{#if opened && filteredItems.length > 0}
|
||||
<div class="autocomplete-list" use:floatingContent bind:this={listElm}>
|
||||
<div bind:this={listElm} class="autocomplete-list" use:floatingContent>
|
||||
{#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) => {
|
||||
e.key == "Enter" && selectListItem(item, true);
|
||||
if (e.key === "Enter") {
|
||||
selectListItem(item, true);
|
||||
}
|
||||
}}
|
||||
on:pointerenter={() => {
|
||||
highlightIndex = i;
|
||||
}}
|
||||
>
|
||||
{#if item.icon}
|
||||
<Icon size={1.2} path={item.icon} />
|
||||
<Icon path={item.icon} size={1.2} />
|
||||
{/if}
|
||||
{item.name}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import EntryFilterChip from "./FilterChip.svelte";
|
||||
|
||||
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 type {
|
||||
FilterDef,
|
||||
FilterQdata,
|
||||
|
@ -10,13 +16,11 @@
|
|||
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 = undefined;
|
||||
export let filterData: FilterQdata | null | 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) */
|
||||
|
@ -26,9 +30,9 @@
|
|||
|
||||
let autocomplete: Autocomplete<BaseItem> | undefined;
|
||||
let activeFilters: FilterData[] = [];
|
||||
let cache: { [key: string]: BaseItem[] } = {};
|
||||
const cache: { [key: string]: BaseItem[] } = {};
|
||||
let searchVal = "";
|
||||
let searchDebounce = new Debouncer(400, () => {
|
||||
const searchDebounce = new Debouncer(400, () => {
|
||||
onUpdate(getFilterQdata());
|
||||
});
|
||||
|
||||
|
@ -38,7 +42,9 @@
|
|||
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];
|
||||
|
@ -46,9 +52,12 @@
|
|||
|
||||
// Filter menu items to be hidden
|
||||
$: hiddenIds = new Set([
|
||||
...Object.values(FILTERS).flatMap((f) =>
|
||||
f.inputType === 2 || activeFilters.every((af) => af.id !== f.id) ? [] : [f.id]
|
||||
),
|
||||
...Object.values(FILTERS).flatMap((f) => {
|
||||
return f.inputType === 2
|
||||
|| activeFilters.every((af) => af.id !== f.id)
|
||||
? []
|
||||
: [f.id];
|
||||
}),
|
||||
...hiddenFilters,
|
||||
]);
|
||||
|
||||
|
@ -57,9 +66,9 @@
|
|||
updateFromQueryData(filterData ?? {});
|
||||
}
|
||||
|
||||
function updateFromQueryData(filterData: FilterQdata) {
|
||||
function updateFromQueryData(fd: FilterQdata) {
|
||||
const filters: FilterData[] = [];
|
||||
for (const [id, value] of Object.entries(filterData)) {
|
||||
for (const [id, value] of Object.entries(fd)) {
|
||||
// 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
|
||||
|
@ -74,7 +83,7 @@
|
|||
});
|
||||
});
|
||||
} else {
|
||||
let selection: SelectionOrText = {};
|
||||
const selection: SelectionOrText = {};
|
||||
if (typeof value === "string") selection.name = value;
|
||||
else if (typeof value === "boolean") selection.toggle = value;
|
||||
else selection.id = value;
|
||||
|
@ -88,13 +97,10 @@
|
|||
function getHiddenIds(fid: string, fpos: number): Set<string | number> {
|
||||
if (FILTERS[fid].inputType === 2) {
|
||||
return new Set(
|
||||
activeFilters.flatMap((f, i) => {
|
||||
return i !== fpos && f.selection?.id ? [f.selection?.id] : [];
|
||||
})
|
||||
activeFilters.flatMap((f, i) => (i !== fpos && f.selection?.id ? [f.selection?.id] : [])),
|
||||
);
|
||||
} else {
|
||||
return new Set();
|
||||
}
|
||||
return new Set();
|
||||
}
|
||||
|
||||
function focusInput() {
|
||||
|
@ -102,23 +108,21 @@
|
|||
}
|
||||
|
||||
function getFilterQdata(): FilterQdata | undefined {
|
||||
let fd: FilterQdata = {};
|
||||
const fd: FilterQdata = {};
|
||||
activeFilters.forEach((fdata) => {
|
||||
const filter = FILTERS[fdata.id];
|
||||
const key = filter.id;
|
||||
|
||||
let val = null;
|
||||
// Valueless filter (val = true)
|
||||
if (filter.inputType === 0) {
|
||||
// Valueless filter (val = true)
|
||||
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
|
||||
// @ts-expect-error TODO
|
||||
val = { id: fdata.selection.id, name: fdata.selection.name };
|
||||
} else if (filter.inputType === 3) {
|
||||
val = Boolean(fdata.selection?.toggle);
|
||||
|
@ -126,9 +130,9 @@
|
|||
|
||||
if (val !== null && val !== undefined) {
|
||||
if (filter.inputType === 2) {
|
||||
//@ts-expect-error fd[key] is checked
|
||||
// @ts-expect-error fd[key] is checked
|
||||
if (Array.isArray(fd[key])) fd[key].push(val);
|
||||
//@ts-expect-error fd[key] is checked
|
||||
// @ts-expect-error fd[key] is checked
|
||||
else fd[key] = [val];
|
||||
} else {
|
||||
fd[key] = val;
|
||||
|
@ -137,7 +141,7 @@
|
|||
});
|
||||
|
||||
if (searchVal) {
|
||||
fd["search"] = searchVal;
|
||||
fd.search = searchVal;
|
||||
}
|
||||
|
||||
if (Object.keys(fd).length === 0) return undefined;
|
||||
|
@ -163,9 +167,8 @@
|
|||
}
|
||||
|
||||
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();
|
||||
|
@ -190,10 +193,10 @@
|
|||
<div class="filterbar-inner input input-sm input-bordered">
|
||||
{#each activeFilters as fdata, i}
|
||||
<EntryFilterChip
|
||||
filter={FILTERS[fdata.id]}
|
||||
{fdata}
|
||||
hiddenIds={() => getHiddenIds(fdata.id, i)}
|
||||
{cache}
|
||||
{fdata}
|
||||
filter={FILTERS[fdata.id]}
|
||||
hiddenIds={() => getHiddenIds(fdata.id, i)}
|
||||
onRemove={() => removeFilter(i)}
|
||||
onSelection={(sel, kb) => {
|
||||
updateFilter();
|
||||
|
@ -204,19 +207,19 @@
|
|||
<Autocomplete
|
||||
bind:this={autocomplete}
|
||||
cls="mr-8"
|
||||
items={filterMenuItems}
|
||||
{hiddenIds}
|
||||
placeholder="Filter"
|
||||
onSelect={(item) => {
|
||||
const close = addFilter(item);
|
||||
return { newValue: "", close };
|
||||
}}
|
||||
items={filterMenuItems}
|
||||
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"
|
||||
|
@ -227,14 +230,14 @@
|
|||
updateFilter();
|
||||
}}
|
||||
>
|
||||
<Icon size={1.2} path={mdiClose} />
|
||||
<Icon path={mdiClose} size={1.2} />
|
||||
</button>
|
||||
</div>
|
||||
{#if search}
|
||||
<input
|
||||
class="input input-sm input-bordered"
|
||||
type="text"
|
||||
placeholder="Suche"
|
||||
type="text"
|
||||
bind:value={searchVal}
|
||||
on:input={onSearchInput}
|
||||
on:keypress={onSearchKeypress}
|
||||
|
@ -250,6 +253,9 @@
|
|||
|
||||
.filterbar-inner {
|
||||
@apply flex flex-wrap flex-grow items-stretch h-min p-0 gap-2 relative;
|
||||
line-height: 30px;
|
||||
|
||||
:global(input) {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
<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();
|
||||
|
@ -44,7 +48,7 @@
|
|||
|
||||
<div
|
||||
class="flex items-center overflow-hidden rounded-md bg-primary text-primary-content
|
||||
gap-1 pl-1"
|
||||
gap-1 pl-1"
|
||||
>
|
||||
<button
|
||||
class="flex items-center gap-1"
|
||||
|
@ -59,7 +63,7 @@ gap-1 pl-1"
|
|||
}}
|
||||
>
|
||||
{#if filterIcon}
|
||||
<Icon size={1.2} path={filterIcon} />
|
||||
<Icon path={filterIcon} size={1.2} />
|
||||
{/if}
|
||||
<span class="flex items-center">
|
||||
{filterName + (hasInputField ? ":" : "")}
|
||||
|
@ -72,33 +76,32 @@ gap-1 pl-1"
|
|||
{@const hids = hiddenIds()}
|
||||
<Autocomplete
|
||||
bind:this={autocomplete}
|
||||
items={filter.options ?? []}
|
||||
hiddenIds={hids}
|
||||
{cache}
|
||||
cacheKey={filter.id}
|
||||
selection={fdata.selection?.id
|
||||
? { id: fdata.selection?.id, name: fdata.selection?.name ?? "" }
|
||||
: null}
|
||||
padding={false}
|
||||
hiddenIds={hids}
|
||||
items={filter.options ?? []}
|
||||
onBackspace={onRemove}
|
||||
{onClose}
|
||||
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 ?? "" };
|
||||
}
|
||||
return { close: false, newValue: item.name ?? "" };
|
||||
}}
|
||||
{onClose}
|
||||
onBackspace={onRemove}
|
||||
padding={false}
|
||||
partOfFilterbar
|
||||
selection={fdata.selection?.id
|
||||
? { id: fdata.selection?.id, name: fdata.selection?.name ?? "" }
|
||||
: null}
|
||||
/>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
class="bg-transparent"
|
||||
type="text"
|
||||
autofocus
|
||||
type="text"
|
||||
value={fdata.selection?.name ?? ""}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === "Escape") onClose(true);
|
||||
|
@ -129,6 +132,6 @@ gap-1 pl-1"
|
|||
aria-label={`Filter "${filterName}" entfernen"`}
|
||||
on:click={onRemove}
|
||||
>
|
||||
<Icon size={1} path={mdiClose} />
|
||||
<Icon path={mdiClose} size={1} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { trpc } from "$lib/shared/trpc";
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiAccountInjury,
|
||||
|
@ -13,6 +12,9 @@ import {
|
|||
mdiMagnify,
|
||||
mdiTag,
|
||||
} from "@mdi/js";
|
||||
|
||||
import { trpc } from "$lib/shared/trpc";
|
||||
|
||||
import type { FilterDef } from "./types";
|
||||
|
||||
export const ENTRY_FILTERS: { [key: string]: FilterDef } = {
|
||||
|
@ -21,54 +23,42 @@ export const ENTRY_FILTERS: { [key: string]: FilterDef } = {
|
|||
name: "Kategorie",
|
||||
icon: mdiTag,
|
||||
inputType: 2,
|
||||
options: async () => {
|
||||
return await trpc().category.list.query();
|
||||
},
|
||||
options: async () => trpc().category.list.query(),
|
||||
},
|
||||
author: {
|
||||
id: "author",
|
||||
name: "Autor",
|
||||
icon: mdiAccount,
|
||||
inputType: 2,
|
||||
options: async () => {
|
||||
return await trpc().user.getNames.query();
|
||||
},
|
||||
options: async () => trpc().user.getNames.query(),
|
||||
},
|
||||
executor: {
|
||||
id: "executor",
|
||||
name: "Erledigt von",
|
||||
icon: mdiDoctor,
|
||||
inputType: 2,
|
||||
options: async () => {
|
||||
return await trpc().user.getNames.query();
|
||||
},
|
||||
options: async () => trpc().user.getNames.query(),
|
||||
},
|
||||
patient: {
|
||||
id: "patient",
|
||||
name: "Patient",
|
||||
icon: mdiAccountInjury,
|
||||
inputType: 2,
|
||||
options: async () => {
|
||||
return await trpc().patient.getNames.query();
|
||||
},
|
||||
options: async () => trpc().patient.getNames.query(),
|
||||
},
|
||||
station: {
|
||||
id: "station",
|
||||
name: "Station",
|
||||
icon: mdiDomain,
|
||||
inputType: 2,
|
||||
options: async () => {
|
||||
return await trpc().station.list.query();
|
||||
},
|
||||
options: async () => trpc().station.list.query(),
|
||||
},
|
||||
room: {
|
||||
id: "room",
|
||||
name: "Zimmer",
|
||||
icon: mdiBedKingOutline,
|
||||
inputType: 2,
|
||||
options: async () => {
|
||||
return await trpc().room.list.query();
|
||||
},
|
||||
options: async () => trpc().room.list.query(),
|
||||
},
|
||||
done: {
|
||||
id: "done",
|
||||
|
@ -100,18 +90,14 @@ export const PATIENT_FILTER: { [key: string]: FilterDef } = {
|
|||
name: "Station",
|
||||
icon: mdiDomain,
|
||||
inputType: 2,
|
||||
options: async () => {
|
||||
return await trpc().station.list.query();
|
||||
},
|
||||
options: async () => trpc().station.list.query(),
|
||||
},
|
||||
room: {
|
||||
id: "room",
|
||||
name: "Zimmer",
|
||||
icon: mdiBedKingOutline,
|
||||
inputType: 2,
|
||||
options: async () => {
|
||||
return await trpc().room.list.query();
|
||||
},
|
||||
options: async () => trpc().room.list.query(),
|
||||
},
|
||||
hidden: {
|
||||
id: "hidden",
|
||||
|
|
68
src/lib/components/form/CategoryForm.svelte
Normal file
68
src/lib/components/form/CategoryForm.svelte
Normal file
|
@ -0,0 +1,68 @@
|
|||
<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,12 +1,15 @@
|
|||
<script lang="ts">
|
||||
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 { browser } from "$app/environment";
|
||||
|
||||
import { defaults, superForm } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import { ZPatientNew } from "$lib/shared/model/validation";
|
||||
import { browser } from "$app/environment";
|
||||
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";
|
||||
|
||||
const schema = zod(ZPatientNew);
|
||||
|
||||
|
@ -15,8 +18,11 @@
|
|||
|
||||
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>
|
||||
|
||||
|
@ -28,31 +34,31 @@
|
|||
{/if}
|
||||
</h1>
|
||||
|
||||
<form method="POST" class="flex flex-col gap-4" use:enhance>
|
||||
<form class="flex flex-col gap-4" method="POST" use:enhance>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<FormField label="Vorname" errors={$errors.first_name}>
|
||||
<FormField errors={$errors.first_name} label="Vorname">
|
||||
<input
|
||||
type="text"
|
||||
name="first_name"
|
||||
aria-invalid={Boolean($errors.first_name)}
|
||||
type="text"
|
||||
bind:value={$form.first_name}
|
||||
{...$constraints.last_name}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Nachname" errors={$errors.last_name}>
|
||||
<FormField errors={$errors.last_name} label="Nachname">
|
||||
<input
|
||||
type="text"
|
||||
name="last_name"
|
||||
aria-invalid={Boolean($errors.last_name)}
|
||||
type="text"
|
||||
bind:value={$form.last_name}
|
||||
{...$constraints.last_name}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Alter" errors={$errors.age}>
|
||||
<FormField errors={$errors.age} label="Alter">
|
||||
<input
|
||||
type="number"
|
||||
name="age"
|
||||
aria-invalid={Boolean($errors.age)}
|
||||
type="number"
|
||||
bind:value={$form.age}
|
||||
{...$constraints.age}
|
||||
/>
|
||||
|
@ -60,13 +66,11 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<FormField label="Zimmer" errors={$errors.room_id}>
|
||||
<FormField errors={$errors.room_id} label="Zimmer">
|
||||
<Autocomplete
|
||||
idInputName="room_id"
|
||||
inputCls="input input-bordered w-full max-w-xs"
|
||||
items={async () => {
|
||||
return await trpc().room.list.query();
|
||||
}}
|
||||
selection={patient?.room}
|
||||
items={async () => trpc().room.list.query()}
|
||||
onSelect={(item) => {
|
||||
station = item.station;
|
||||
$form.room_id = item.id;
|
||||
|
@ -74,15 +78,15 @@
|
|||
onUnselect={() => {
|
||||
$form.room_id = null;
|
||||
}}
|
||||
idInputName="room_id"
|
||||
selection={patient?.room}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Station">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
disabled
|
||||
type="text"
|
||||
value={station?.name ?? ""}
|
||||
/>
|
||||
</FormField>
|
||||
|
@ -91,8 +95,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>
|
||||
|
|
66
src/lib/components/form/RoomForm.svelte
Normal file
66
src/lib/components/form/RoomForm.svelte
Normal file
|
@ -0,0 +1,66 @@
|
|||
<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>
|
50
src/lib/components/form/StationForm.svelte
Normal file
50
src/lib/components/form/StationForm.svelte
Normal file
|
@ -0,0 +1,50 @@
|
|||
<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,6 +6,7 @@
|
|||
|
||||
export let category: Category;
|
||||
export let baseUrl = URL_ENTRIES;
|
||||
export let href: string | undefined = undefined;
|
||||
|
||||
$: textColor = category.color
|
||||
? colorToHex(getTextColor(hexToColor(category.color)))
|
||||
|
@ -18,17 +19,26 @@
|
|||
category: [{ id: category.id, name: category.name }],
|
||||
},
|
||||
},
|
||||
baseUrl
|
||||
baseUrl,
|
||||
);
|
||||
e.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
{#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 = undefined;
|
||||
export let sortData: SortRequest | 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 title="ID" key="id" {sortData} {sortUpdate} />
|
||||
<SortHeader key="id" {sortData} {sortUpdate} title="ID" />
|
||||
{#if !perPatient}
|
||||
<SortHeader title="Patient" key="patient" {sortData} {sortUpdate} />
|
||||
<SortHeader title="Zimmer" key="room" {sortData} {sortUpdate} />
|
||||
<SortHeader key="patient" {sortData} {sortUpdate} title="Patient" />
|
||||
<SortHeader key="room" {sortData} {sortUpdate} title="Zimmer" />
|
||||
{/if}
|
||||
<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} />
|
||||
<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" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each entries.items as entry}
|
||||
{#each entries.items as entry (entry.id)}
|
||||
<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"
|
||||
href="/entry/{entry.id}"
|
||||
aria-label="Eintrag anzeigen">{entry.id}</a
|
||||
aria-label="Eintrag anzeigen"
|
||||
href="/entry/{entry.id}">{entry.id}</a
|
||||
></td
|
||||
>
|
||||
{#if !perPatient}
|
||||
<td><PatientField patient={entry.patient} {baseUrl} /></td>
|
||||
<td><PatientField {baseUrl} patient={entry.patient} /></td>
|
||||
<td>
|
||||
{#if entry.patient.room}
|
||||
<RoomField room={entry.patient.room} {baseUrl} />
|
||||
<RoomField {baseUrl} room={entry.patient.room} />
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
<td>
|
||||
{#if entry.current_version.category}
|
||||
<CategoryField category={entry.current_version.category} {baseUrl} />
|
||||
<CategoryField {baseUrl} category={entry.current_version.category} />
|
||||
{/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 user={entry.current_version.author} {baseUrl} /></td>
|
||||
<td><UserField {baseUrl} user={entry.current_version.author} /></td>
|
||||
<td><span class="line-clamp-2">{entry.current_version.text}</span></td>
|
||||
<td>
|
||||
{#if entry.execution}
|
||||
{formatDate(entry.execution.created_at, true)}
|
||||
<UserField
|
||||
user={entry.execution.author}
|
||||
filterName="executor"
|
||||
{baseUrl}
|
||||
filterName="executor"
|
||||
user={entry.execution.author}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
<script lang="ts">
|
||||
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";
|
||||
|
||||
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";
|
||||
|
||||
export let query: z.infer<typeof ZEntriesQuery>;
|
||||
export let entries: RouterOutput["entry"]["list"];
|
||||
export let baseUrl: string;
|
||||
|
@ -50,23 +54,23 @@
|
|||
|
||||
<FilterBar
|
||||
FILTERS={ENTRY_FILTERS}
|
||||
hiddenFilters={patientId !== null ? ["patient"] : []}
|
||||
filterData={query.filter}
|
||||
hiddenFilters={patientId !== null ? ["patient"] : []}
|
||||
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,17 +1,21 @@
|
|||
<script lang="ts">
|
||||
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";
|
||||
|
||||
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";
|
||||
|
||||
export let query: z.infer<typeof ZPatientsQuery>;
|
||||
export let patients: RouterOutput["patient"]["list"];
|
||||
export let baseUrl: string;
|
||||
|
@ -54,10 +58,10 @@
|
|||
<slot />
|
||||
</FilterBar>
|
||||
|
||||
<PatientTable {patients} sortData={query.sort} {sortUpdate} {baseUrl} />
|
||||
<PatientTable {baseUrl} {patients} sortData={query.sort} {sortUpdate} />
|
||||
|
||||
<PaginationButtons
|
||||
paginationData={query.pagination}
|
||||
data={patients}
|
||||
onUpdate={paginationUpdate}
|
||||
paginationData={query.pagination}
|
||||
/>
|
||||
|
|
|
@ -14,12 +14,14 @@
|
|||
],
|
||||
},
|
||||
},
|
||||
baseUrl
|
||||
baseUrl,
|
||||
);
|
||||
e.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="ellipsis" on:click={onClick}
|
||||
>{`${patient.first_name} ${patient.last_name}`}</button
|
||||
<button
|
||||
class="ellipsis"
|
||||
on:click={onClick}
|
||||
>{`${patient.first_name} ${patient.last_name}`}</button
|
||||
>
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
<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 = undefined;
|
||||
export let sortData: SortRequest | undefined;
|
||||
export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
|
||||
export let baseUrl: string;
|
||||
</script>
|
||||
|
@ -19,17 +21,17 @@
|
|||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<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} />
|
||||
<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" />
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each patients.items as patient}
|
||||
{@const full_name = patient.first_name + " " + patient.last_name}
|
||||
{#each patients.items as patient (patient.id)}
|
||||
{@const full_name = `${patient.first_name} ${patient.last_name}`}
|
||||
<tr
|
||||
class="transition-colors hover:bg-neutral-content/10"
|
||||
class:p-hidden={patient.hidden}
|
||||
|
@ -37,15 +39,15 @@
|
|||
<td
|
||||
><a
|
||||
class="btn btn-xs btn-primary"
|
||||
href="/patient/{patient.id}"
|
||||
aria-label="Eintrag anzeigen">{patient.id}</a
|
||||
aria-label="Eintrag anzeigen"
|
||||
href="/patient/{patient.id}">{patient.id}</a
|
||||
></td
|
||||
>
|
||||
<td>{full_name}</td>
|
||||
<td>{patient.age ?? ""}</td>
|
||||
<td>
|
||||
{#if patient.room}
|
||||
<RoomField room={patient.room} {baseUrl} />
|
||||
<RoomField {baseUrl} room={patient.room} />
|
||||
{/if}
|
||||
</td>
|
||||
<td>{formatDate(patient.created_at, true)}</td>
|
||||
|
@ -55,14 +57,14 @@
|
|||
on:click={() => {
|
||||
gotoEntityQuery(
|
||||
{ filter: { patient: [{ id: patient.id, name: full_name }] } },
|
||||
URL_ENTRIES
|
||||
URL_ENTRIES,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon size={1.2} path={mdiFilter} />
|
||||
<Icon path={mdiFilter} size={1.2} />
|
||||
</button>
|
||||
<button class="btn btn-circle btn-ghost btn-xs inline">
|
||||
<Icon size={1.2} path={mdiClose} />
|
||||
<Icon path={mdiClose} size={1.2} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
room: [{ id: room.id, name: room.name }],
|
||||
},
|
||||
},
|
||||
baseUrl
|
||||
baseUrl,
|
||||
);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
<script lang="ts">
|
||||
import type { SortRequest } from "$lib/shared/model";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import { mdiSortAscending, mdiSortDescending } from "@mdi/js";
|
||||
|
||||
import type { SortRequest } from "$lib/shared/model";
|
||||
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
|
||||
export let key: string;
|
||||
export let title: string;
|
||||
export let sortData: SortRequest | undefined = undefined;
|
||||
export let sortData: SortRequest | undefined;
|
||||
export let sortUpdate: (sort: SortRequest | undefined) => void = () => {};
|
||||
|
||||
// 1: asc, 2: desc, 0: not sorted
|
||||
$: sorting = sortData?.field === key ? (sortData.asc !== false ? 1 : 2) : 0;
|
||||
let sorting = 0;
|
||||
$: if (sortData?.field === key) {
|
||||
sorting = sortData.asc !== false ? 1 : 2;
|
||||
} else {
|
||||
sorting = 0;
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
if (sorting === 2) {
|
||||
|
@ -23,7 +30,7 @@
|
|||
<th>
|
||||
<button class:text-primary={sorting > 0} on:click={onClick}>
|
||||
{#if sorting > 0}
|
||||
<Icon size={1} path={sorting === 1 ? mdiSortAscending : mdiSortDescending} />
|
||||
<Icon path={sorting === 1 ? mdiSortAscending : mdiSortDescending} size={1} />
|
||||
{/if}
|
||||
{title}
|
||||
</button>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
export let filterName: string = "author";
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
let query: EntityQuery = { filter: {} };
|
||||
const 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} {type} class="input input-bordered w-full max-w-xs" {value} />
|
||||
<input {name} class="input input-bordered w-full max-w-xs" {type} {value} />
|
||||
</label>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import type { TRPCErrorResponse } from "$lib/shared/model";
|
||||
|
||||
export let error: TRPCErrorResponse;
|
||||
export let status: number | undefined = undefined;
|
||||
export let status: number | 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 href={backHref} class="btn btn-sm btn-circle btn-ghost">
|
||||
<a class="btn btn-sm btn-circle btn-ghost" href={backHref}>
|
||||
<Icon path={mdiChevronLeft} size={1.8} />
|
||||
</a>
|
||||
{/if}
|
||||
|
|
|
@ -15,10 +15,14 @@
|
|||
</script>
|
||||
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
style="--color: {color}; --size: {size}rem; --rotate: {rotate}deg; --scaleV: {scaleV}; --scaleH:
|
||||
{scaleH}; --spin-duration: {absSpin}s;"
|
||||
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"
|
||||
>
|
||||
{#if title}<title>{title}</title>{/if}
|
||||
|
||||
|
@ -27,7 +31,7 @@
|
|||
</g>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
<style lang="postcss">
|
||||
svg {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
|
@ -36,12 +40,14 @@
|
|||
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,7 +12,8 @@
|
|||
showProgress = true;
|
||||
showError = false;
|
||||
navInterval = setInterval(() => {
|
||||
navprogress += navprogress <= 90 ? 2.5 : navprogress < 95 ? 0.1 : 0;
|
||||
if (navprogress <= 90) navprogress += 2.5;
|
||||
else if (navprogress < 95) navprogress += 0.1;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
@ -36,8 +37,8 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class={cls}
|
||||
style:width={`${navprogress}%`}
|
||||
class={cls}
|
||||
class:active={alwaysShown || showProgress}
|
||||
class:error={showError}
|
||||
/>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<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>
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
<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,11 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { mdiChevronLeft, mdiChevronRight, mdiPageFirst, mdiPageLast } from "@mdi/js";
|
||||
import {
|
||||
mdiChevronLeft, mdiChevronRight, mdiPageFirst, mdiPageLast,
|
||||
} from "@mdi/js";
|
||||
|
||||
import type { Pagination, PaginationRequest } from "$lib/shared/model";
|
||||
import { PAGINATION_LIMIT } from "$lib/shared/constants";
|
||||
import type { Pagination, PaginationRequest } from "$lib/shared/model";
|
||||
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
export let paginationData: PaginationRequest | null | undefined = undefined;
|
||||
export let paginationData: PaginationRequest | null | undefined;
|
||||
export let data: Pagination<unknown>;
|
||||
export let onUpdate: (pagination: PaginationRequest) => void = () => {};
|
||||
|
||||
|
@ -31,13 +34,13 @@
|
|||
function getPaginationRequest(page: number): PaginationRequest | null {
|
||||
if (page < 1 || page > nPages) return null;
|
||||
|
||||
let pag = paginationData ? structuredClone(paginationData) : {};
|
||||
const pag = paginationData ? structuredClone(paginationData) : {};
|
||||
pag.offset = (page - 1) * limit;
|
||||
return pag;
|
||||
}
|
||||
|
||||
function pagClick(page: number) {
|
||||
let pag = getPaginationRequest(page);
|
||||
const pag = getPaginationRequest(page);
|
||||
if (pag) onUpdate(pag);
|
||||
}
|
||||
</script>
|
||||
|
@ -51,7 +54,9 @@
|
|||
<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}
|
||||
|
@ -62,10 +67,14 @@
|
|||
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,6 +1,7 @@
|
|||
<!-- Button showing the amount of entry versions -->
|
||||
<script lang="ts">
|
||||
import { mdiHistory } from "@mdi/js";
|
||||
|
||||
import Icon from "./Icon.svelte";
|
||||
|
||||
export let n: number;
|
||||
|
@ -8,7 +9,7 @@
|
|||
</script>
|
||||
|
||||
{#if n > 1}
|
||||
<a {href} class="btn btn-xs btn-primary rounded-full">
|
||||
<a class="btn btn-xs btn-primary rounded-full" {href}>
|
||||
<Icon path={mdiHistory} size={1.2} />
|
||||
<span>{n}</span>
|
||||
</a>
|
||||
|
|
10
src/lib/components/ui/markdown/Markdown.svelte
Normal file
10
src/lib/components/ui/markdown/Markdown.svelte
Normal file
|
@ -0,0 +1,10 @@
|
|||
<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,7 +1,11 @@
|
|||
<script lang="ts">
|
||||
import Markdown from "./Markdown.svelte";
|
||||
import "./carta.pcss";
|
||||
|
||||
import { Carta, MarkdownEditor, type Labels } from "carta-md";
|
||||
import type { InputConstraint } from "sveltekit-superforms";
|
||||
|
||||
import { CARTA_CFG } from "./carta";
|
||||
|
||||
export let label = "";
|
||||
export let name = "";
|
||||
export let value = "";
|
||||
|
@ -10,32 +14,34 @@
|
|||
export let constraints: InputConstraint | undefined = undefined;
|
||||
export let marginTop = false;
|
||||
|
||||
let editMode = true;
|
||||
const carta = new Carta(CARTA_CFG);
|
||||
|
||||
function toggle() {
|
||||
editMode = !editMode;
|
||||
}
|
||||
const LABELS: Labels = {
|
||||
writeTab: "Bearbeiten",
|
||||
previewTab: "Vorschau",
|
||||
iconsLabels: {
|
||||
heading: "Titel",
|
||||
bold: "Fett",
|
||||
italic: "Kursiv",
|
||||
strikethrough: "Durchgestrichen",
|
||||
quote: "Zitat",
|
||||
bulletedList: "Aufzählung",
|
||||
numberedList: "Nummerierung",
|
||||
},
|
||||
};
|
||||
</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">
|
||||
{#if editMode}
|
||||
<textarea
|
||||
class="textarea w-full h-48"
|
||||
{name}
|
||||
aria-invalid={ariaInvalid}
|
||||
bind:value
|
||||
{...constraints}
|
||||
/>
|
||||
{:else}
|
||||
<Markdown src={value} />
|
||||
{/if}
|
||||
<MarkdownEditor
|
||||
{carta}
|
||||
labels={LABELS}
|
||||
mode="tabs"
|
||||
textarea={{ name, "aria-invalid": ariaInvalid, ...constraints }}
|
||||
bind:value />
|
||||
|
||||
{#if errors}
|
||||
<div class="label flex-col items-start">
|
123
src/lib/components/ui/markdown/carta.pcss
Normal file
123
src/lib/components/ui/markdown/carta.pcss
Normal file
|
@ -0,0 +1,123 @@
|
|||
.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;
|
||||
}
|
9
src/lib/components/ui/markdown/carta.ts
Normal file
9
src/lib/components/ui/markdown/carta.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
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,12 +7,13 @@ 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 = {
|
||||
|
@ -61,14 +62,14 @@ ISC License
|
|||
*/
|
||||
|
||||
function authjsUrl(event: RequestEvent, authjsEndpoint: string): string {
|
||||
const url = event.url;
|
||||
return url.protocol + "//" + url.host + AUTH_BASE_PATH + "/" + authjsEndpoint;
|
||||
const { url } = event;
|
||||
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");
|
||||
|
@ -112,12 +113,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);
|
||||
}
|
||||
event.locals.session = await auth(event);
|
||||
|
||||
return resolve(event);
|
||||
};
|
||||
|
|
|
@ -33,19 +33,16 @@ 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 },
|
||||
|
@ -53,12 +50,14 @@ 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),
|
||||
|
@ -72,10 +71,9 @@ 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,9 +2,8 @@ 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,4 +1,5 @@
|
|||
import type { Category, CategoryNew } from "$lib/shared/model";
|
||||
|
||||
import { prisma } from "$lib/server/prisma";
|
||||
|
||||
export async function newCategory(category: CategoryNew): Promise<number> {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { prisma } from "$lib/server/prisma";
|
||||
import type {
|
||||
EntriesFilter,
|
||||
Entry,
|
||||
|
@ -13,6 +12,9 @@ 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";
|
||||
|
||||
|
@ -86,7 +88,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({
|
||||
|
@ -117,10 +119,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;
|
||||
}
|
||||
|
@ -137,7 +139,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({
|
||||
|
@ -153,8 +155,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");
|
||||
}
|
||||
|
@ -179,7 +181,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
|
||||
|
@ -240,14 +242,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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -268,7 +270,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,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -330,8 +332,7 @@ 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) => {
|
||||
return {
|
||||
const items: Entry[] = res.map((item) => ({
|
||||
id: item.id,
|
||||
patient: {
|
||||
id: item.patient_id,
|
||||
|
@ -373,8 +374,7 @@ left join stations s on s.id = r.station_id`
|
|||
created_at: item.execution_created_at,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
return { items, offset: qb.getOffset(), total };
|
||||
}
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
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,
|
||||
|
@ -10,16 +21,6 @@ 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,3 +1,5 @@
|
|||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
|
||||
import type {
|
||||
Patient,
|
||||
PatientNew,
|
||||
|
@ -7,11 +9,12 @@ 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 } });
|
||||
|
@ -64,13 +67,11 @@ export async function getPatientNames(): Promise<PatientTag[]> {
|
|||
where: { hidden: false },
|
||||
orderBy: { last_name: "asc" },
|
||||
});
|
||||
return patients.map((p) => {
|
||||
return {
|
||||
return patients.map((p) => ({
|
||||
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> {
|
||||
|
@ -80,21 +81,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`);
|
||||
}
|
||||
|
@ -144,8 +145,7 @@ export async function getPatients(
|
|||
])) as [RowItem[], { count: bigint }[]];
|
||||
|
||||
const total = Number(countRes[0].count);
|
||||
const items: Patient[] = res.map((patient) => {
|
||||
return {
|
||||
const items: Patient[] = res.map((patient) => ({
|
||||
id: patient.id,
|
||||
first_name: patient.first_name,
|
||||
last_name: patient.last_name,
|
||||
|
@ -162,8 +162,7 @@ export async function getPatients(
|
|||
},
|
||||
}
|
||||
: null,
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
return {
|
||||
items,
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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> {
|
||||
|
@ -16,7 +20,7 @@ export async function deleteStation(id: number) {
|
|||
}
|
||||
|
||||
export async function getStation(id: number): Promise<Station> {
|
||||
return await prisma.station.findUniqueOrThrow({ where: { id } });
|
||||
return prisma.station.findUniqueOrThrow({ where: { id } });
|
||||
}
|
||||
|
||||
export async function getStations(): Promise<Station[]> {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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 } });
|
||||
|
@ -14,7 +16,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,4 +1,5 @@
|
|||
import { expect, test } from "vitest";
|
||||
|
||||
import { QueryBuilder, parseSearchQuery } from "./util";
|
||||
|
||||
test("query builder", () => {
|
||||
|
@ -9,7 +10,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();
|
||||
|
@ -20,7 +21,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([
|
||||
{
|
||||
|
@ -52,6 +53,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,15 +94,14 @@ 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;
|
||||
}
|
||||
|
@ -112,12 +111,19 @@ 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) {
|
||||
|
@ -136,7 +142,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);
|
||||
}
|
||||
|
||||
|
@ -168,7 +174,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) {
|
||||
|
@ -183,7 +189,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,7 +1,8 @@
|
|||
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,7 +1,8 @@
|
|||
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) {
|
||||
|
@ -18,7 +19,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,4 +1,5 @@
|
|||
import { initTRPC } from "@trpc/server";
|
||||
|
||||
import type { Context } from "./context";
|
||||
|
||||
export { trpcWrap } from "$lib/server/trpc/handleError";
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { t } from ".";
|
||||
|
||||
import { categoryRouter } from "./routes/category";
|
||||
import { entryRouter } from "./routes/entry";
|
||||
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";
|
||||
|
||||
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 { userRouter } from "./routes/user";
|
||||
|
||||
export const router = t.router({
|
||||
greeting: t.procedure.query(async () => {
|
||||
return `Hello tRPC @ ${new Date().toLocaleTimeString()}`;
|
||||
}),
|
||||
greeting: t.procedure.query(
|
||||
async () => `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 { fields, ZCategoryNew } from "$lib/shared/model/validation";
|
||||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const categoryRouter = t.router({
|
||||
list: t.procedure.query(async () => trpcWrap(getCategories)),
|
||||
create: t.procedure.input(ZCategoryNew).mutation(async (opts) =>
|
||||
trpcWrap(async () => {
|
||||
get: t.procedure
|
||||
.input(ZEntityId)
|
||||
.query(async (opts) => trpcWrap(async () => getCategory(opts.input))),
|
||||
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,4 +1,3 @@
|
|||
import { t, trpcWrap } from "..";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
|
@ -8,6 +7,8 @@ import {
|
|||
ZEntryNew,
|
||||
ZEntryVersionNew,
|
||||
} from "$lib/shared/model/validation";
|
||||
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
|
||||
|
||||
import {
|
||||
getEntries,
|
||||
getEntry,
|
||||
|
@ -19,89 +20,70 @@ import {
|
|||
newEntryExecution,
|
||||
newEntryVersion,
|
||||
} from "$lib/server/query";
|
||||
import { executionsDiff, versionsDiff } from "$lib/shared/util/diff";
|
||||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
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,3 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { fields, ZPatientNew, ZPatientsQuery } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deletePatient,
|
||||
getPatient,
|
||||
|
@ -8,55 +12,46 @@ 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) => {
|
||||
return getPatients(
|
||||
})),
|
||||
list: t.procedure
|
||||
.input(ZPatientsQuery)
|
||||
.query(async (opts) => 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,8 +1,13 @@
|
|||
import { deleteRoom, getRoom, getRooms, updateRoom } from "$lib/server/query";
|
||||
import { fields, ZRoomNew } from "$lib/shared/model/validation";
|
||||
import { t, trpcWrap } from "..";
|
||||
import { z } from "zod";
|
||||
|
||||
import { fields, ZRoomNew } from "$lib/shared/model/validation";
|
||||
|
||||
import {
|
||||
deleteRoom, getRoom, getRooms, newRoom, updateRoom,
|
||||
} from "$lib/server/query";
|
||||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const roomRouter = t.router({
|
||||
|
@ -10,16 +15,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, 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 () => {
|
||||
.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 () => {
|
||||
await deleteRoom(opts.input);
|
||||
})
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
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();
|
||||
|
||||
|
@ -15,16 +19,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, 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 () => {
|
||||
.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 () => {
|
||||
await deleteStation(opts.input);
|
||||
})
|
||||
),
|
||||
})),
|
||||
});
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import { getUser, getUserNames, getUsers } from "$lib/server/query";
|
||||
import { fields, ZPagination } from "$lib/shared/model/validation";
|
||||
import { t, trpcWrap } from "..";
|
||||
import { z } from "zod";
|
||||
|
||||
import { fields, ZPagination } from "$lib/shared/model/validation";
|
||||
|
||||
import { getUser, getUserNames, getUsers } from "$lib/server/query";
|
||||
|
||||
import { t, trpcWrap } from "..";
|
||||
|
||||
const ZEntityId = fields.EntityId();
|
||||
|
||||
export const userRouter = t.router({
|
||||
list: t.procedure
|
||||
.input(z.object({ pagination: ZPagination }).partial())
|
||||
.query(async (opts) => {
|
||||
return getUsers(opts.input.pagination ?? {});
|
||||
}),
|
||||
.query(async (opts) => getUsers(opts.input.pagination ?? {})),
|
||||
getNames: t.procedure.query(getUserNames),
|
||||
get: t.procedure
|
||||
.input(ZEntityId)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { expect, test } from "vitest";
|
||||
|
||||
import { fields } from "./validation";
|
||||
|
||||
test("date string", () => {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { implement } from "$lib/shared/util/zod";
|
||||
|
||||
import type {
|
||||
CategoryNew,
|
||||
EntryExecutionNew,
|
||||
|
@ -9,36 +11,36 @@ import type {
|
|||
PaginationRequest,
|
||||
PatientNew,
|
||||
RoomNew,
|
||||
SortRequest,
|
||||
StationNew,
|
||||
User,
|
||||
} from ".";
|
||||
|
||||
// Fields
|
||||
export const fields = {
|
||||
EntityId: () => z.number().int().nonnegative(),
|
||||
EntityId: () => z.number().int().min(1),
|
||||
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" },
|
||||
),
|
||||
};
|
||||
|
||||
export const ZUrlEntityId = z.coerce.number().int().nonnegative();
|
||||
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 ZUser = implement<User>().with({
|
||||
id: z.coerce.number().int().nonnegative(),
|
||||
|
@ -58,7 +60,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(),
|
||||
});
|
||||
|
@ -92,31 +94,36 @@ export const ZEntryExecutionNew = implement<EntryExecutionNew>().with({
|
|||
});
|
||||
|
||||
export const ZPagination = implement<PaginationRequest>().with({
|
||||
limit: fields.EntityId().optional(),
|
||||
offset: fields.EntityId().optional(),
|
||||
limit: coercedUint.optional(),
|
||||
offset: coercedUint.optional(),
|
||||
});
|
||||
|
||||
export const ZSort = implement<SortRequest>().with({
|
||||
export const ZSort = z.object({
|
||||
field: z.string().optional(),
|
||||
asc: z.boolean().optional(),
|
||||
asc: coercedBool.optional(),
|
||||
});
|
||||
|
||||
const ZFilterListEntry = z.object({
|
||||
id: fields.EntityId(),
|
||||
id: coercedUint,
|
||||
name: fields.NameString().optional(),
|
||||
});
|
||||
const ZFilterList = z.array(ZFilterListEntry);
|
||||
const paginatedQuery = (f: z.ZodTypeAny) =>
|
||||
z.object({ filter: f, pagination: ZPagination, sort: ZSort }).partial();
|
||||
const paginatedQuery = <T extends z.ZodTypeAny>(f: T) => z
|
||||
.object({
|
||||
filter: f,
|
||||
pagination: ZPagination,
|
||||
sort: ZSort,
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const ZEntriesFilter = z
|
||||
.object({
|
||||
author: ZFilterList,
|
||||
category: ZFilterList,
|
||||
done: z.boolean(),
|
||||
done: coercedBool,
|
||||
executor: ZFilterList,
|
||||
patient: ZFilterList,
|
||||
priority: z.boolean(),
|
||||
priority: coercedBool,
|
||||
room: ZFilterList,
|
||||
search: z.string(),
|
||||
station: ZFilterList,
|
||||
|
@ -130,8 +137,8 @@ export const ZPatientsFilter = z
|
|||
search: z.string(),
|
||||
room: ZFilterList,
|
||||
station: ZFilterList,
|
||||
hidden: z.boolean(),
|
||||
includeHidden: z.boolean(),
|
||||
hidden: coercedBool,
|
||||
includeHidden: z.coerce.boolean(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
||||
import type { Router } from "$lib/server/trpc/router";
|
||||
import { createTRPCClient, type TRPCClientInit } from "trpc-sveltekit";
|
||||
|
||||
import type { Router } from "$lib/server/trpc/router";
|
||||
|
||||
export type RouterInput = inferRouterInputs<Router>;
|
||||
export type RouterOutput = inferRouterOutputs<Router>;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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,10 +46,9 @@ 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
|
||||
: Math.pow((primaryColor_sRGB + 0.055) / 1.055, 2.4);
|
||||
: ((primaryColor_sRGB + 0.055) / 1.055) ** 2.4;
|
||||
return primaryColor_linear;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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", () => {
|
||||
|
@ -54,9 +57,6 @@ 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,8 +75,18 @@ 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.",
|
||||
|
@ -94,7 +104,12 @@ 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,4 +1,5 @@
|
|||
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,11 +1,15 @@
|
|||
import { expect, test, vi } from "vitest";
|
||||
import { humanDate } from ".";
|
||||
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 ".";
|
||||
|
||||
const MINUTE = 60000;
|
||||
const HOUR = 3_600_000;
|
||||
const DAY = 24 * HOUR;
|
||||
|
||||
test.each([
|
||||
it.each([
|
||||
{ s: 0, txt: "jetzt gerade" },
|
||||
{ s: -30 * MINUTE, txt: "vor 30 Minuten" },
|
||||
{ s: -11 * HOUR, txt: "vor 11 Stunden" },
|
||||
|
@ -30,3 +34,22 @@ test.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,8 +1,12 @@
|
|||
import { goto } from "$app/navigation";
|
||||
import type { EntityQuery } from "$lib/shared/model";
|
||||
|
||||
import { isRedirect, error } from "@sveltejs/kit";
|
||||
import { TRPCClientError } from "@trpc/client";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import qs from "qs";
|
||||
import { ZodError } from "zod";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
|
||||
import type { EntityQuery } from "$lib/shared/model";
|
||||
|
||||
const LOCALE = "de-DE";
|
||||
|
||||
|
@ -23,13 +27,12 @@ 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;
|
||||
|
@ -46,7 +49,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);
|
||||
|
||||
|
@ -72,25 +75,30 @@ export function formatBool(val: boolean): string {
|
|||
return val ? "Ja" : "Nein";
|
||||
}
|
||||
|
||||
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)));
|
||||
/** 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 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,
|
||||
// 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,
|
||||
};
|
||||
goto(getQueryUrl(nq, basePath));
|
||||
goto(getQueryUrl(newQuery, basePath));
|
||||
return;
|
||||
}
|
||||
}
|
||||
goto(getQueryUrl(q, basePath));
|
||||
goto(getQueryUrl(query, basePath));
|
||||
}
|
||||
|
||||
/** Wrap a page load query to handle occuring errors
|
||||
|
@ -101,7 +109,9 @@ export async function loadWrap<T>(f: () => Promise<T>) {
|
|||
try {
|
||||
return await f();
|
||||
} catch (e) {
|
||||
if (e instanceof TRPCClientError) {
|
||||
if (isRedirect(e)) {
|
||||
throw e;
|
||||
} else if (e instanceof TRPCClientError) {
|
||||
error(e.data?.httpStatus ?? 500, e.message);
|
||||
} else if (e instanceof ZodError) {
|
||||
error(400, e.message);
|
||||
|
@ -114,7 +124,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 {
|
||||
|
@ -123,7 +133,9 @@ 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) {
|
||||
|
@ -145,3 +157,7 @@ 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,9 +1,10 @@
|
|||
<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
|
||||
|
@ -12,35 +13,41 @@
|
|||
>
|
||||
<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
|
||||
path={mdiHome}
|
||||
cls={$page.route.id === "/(app)" ? "text-primary" : ""}
|
||||
path={mdiHome}
|
||||
/></NavLink
|
||||
>
|
||||
<NavLink active={$page.route.id === "/(app)/plan/[[query]]"} href="/plan"
|
||||
<NavLink
|
||||
active={$page.route.id === "/(app)/plan"}
|
||||
href="/plan"
|
||||
>Planung</NavLink
|
||||
>
|
||||
<NavLink active={$page.route.id === "/(app)/visit"} href="/visit">Visite</NavLink>
|
||||
<NavLink active={$page.route.id === "/(app)/patients/[[query]]"} href="/patients"
|
||||
<NavLink
|
||||
active={$page.route.id === "/(app)/patients"}
|
||||
href="/patients"
|
||||
>Patienten</NavLink
|
||||
>
|
||||
</div>
|
||||
<div class="flex-0">
|
||||
{#if $page.data.session?.user}
|
||||
<div class="dropdown dropdown-hover dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-sm btn-ghost">
|
||||
<div class="btn btn-sm btn-ghost" role="button" tabindex="0">
|
||||
<Icon path={mdiAccount} />
|
||||
<span class="hidden md:inline">{$page.data.session.user?.name}</span>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
|
||||
tabindex="0"
|
||||
>
|
||||
<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>
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
</script>
|
||||
|
||||
|
@ -12,15 +12,14 @@
|
|||
<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 href="/plan" class="btn btn-primary">Planung</a>
|
||||
<a class="btn btn-primary" href="/plan">Planung</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,7 +30,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 href="/visit" class="btn btn-primary">Visite</a>
|
||||
<a class="btn btn-primary" href="/visit">Visite</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
39
src/routes/(app)/categories/+page.svelte
Normal file
39
src/routes/(app)/categories/+page.svelte
Normal file
|
@ -0,0 +1,39 @@
|
|||
<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>
|
11
src/routes/(app)/categories/+page.ts
Normal file
11
src/routes/(app)/categories/+page.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
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 };
|
||||
});
|
||||
};
|
35
src/routes/(app)/category/[id]/+page.server.ts
Normal file
35
src/routes/(app)/category/[id]/+page.server.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
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 };
|
||||
}
|
||||
}),
|
||||
};
|
15
src/routes/(app)/category/[id]/+page.svelte
Normal file
15
src/routes/(app)/category/[id]/+page.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<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>
|
18
src/routes/(app)/category/[id]/+page.ts
Normal file
18
src/routes/(app)/category/[id]/+page.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
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 };
|
||||
});
|
||||
};
|
22
src/routes/(app)/category/new/+page.server.ts
Normal file
22
src/routes/(app)/category/new/+page.server.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
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}`);
|
||||
},
|
||||
};
|
6
src/routes/(app)/category/new/+page.svelte
Normal file
6
src/routes/(app)/category/new/+page.svelte
Normal file
|
@ -0,0 +1,6 @@
|
|||
<script lang="ts">
|
||||
import CategoryForm from "$lib/components/form/CategoryForm.svelte";
|
||||
|
||||
</script>
|
||||
|
||||
<CategoryForm />
|
|
@ -1,25 +1,26 @@
|
|||
import { superValidate } from "sveltekit-superforms";
|
||||
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 = {
|
||||
default: async (event) => {
|
||||
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 { SchemaEntryExecution } from "./schema";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => loadWrap(async () => {
|
||||
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,13 +1,17 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { PageData } from "./$types";
|
||||
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";
|
||||
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
||||
|
||||
import { SchemaEntryExecution } from "./schema";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let formData = defaults(SchemaEntryExecution);
|
||||
const formData = defaults(SchemaEntryExecution);
|
||||
const { form, errors, enhance } = superForm(formData, {
|
||||
validators: SchemaEntryExecution,
|
||||
});
|
||||
|
@ -18,14 +22,14 @@
|
|||
{#if !data.entry.execution}
|
||||
<form method="POST" use:enhance>
|
||||
<MarkdownInput
|
||||
label="Eintrag erledigen"
|
||||
name="text"
|
||||
ariaInvalid={Boolean($errors.text)}
|
||||
bind:value={$form.text}
|
||||
errors={$errors.text}
|
||||
label="Eintrag erledigen"
|
||||
bind:value={$form.text}
|
||||
>
|
||||
<div class="row c-vlight">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Erledigt</button>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Erledigt</button>
|
||||
</div>
|
||||
</MarkdownInput>
|
||||
</form>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
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 { superValidate } from "sveltekit-superforms";
|
||||
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 = {
|
||||
default: async (event) => {
|
||||
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 { SchemaNewEntryVersion } from "./schema";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => loadWrap(async () => {
|
||||
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;
|
||||
});
|
||||
|
||||
throw redirect(302, `/entry/${entryId}`);
|
||||
},
|
||||
} satisfies Actions;
|
||||
redirect(302, `/entry/${id}`);
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import Header from "$lib/components/ui/Header.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 { 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 { browser } from "$app/environment";
|
||||
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 { SchemaNewEntryVersion } from "./schema";
|
||||
|
||||
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>
|
||||
|
@ -26,30 +31,28 @@
|
|||
<title>Eintrag #{data.entry.id}</title>
|
||||
</svelte:head>
|
||||
|
||||
<Header title="Eintrag #{data.entry.id} bearbeiten" backHref={basePath}></Header>
|
||||
<Header backHref={basePath} title="Eintrag #{data.entry.id} bearbeiten" />
|
||||
|
||||
<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 user={data.entry.current_version.author} filterName="author" />
|
||||
<UserField filterName="author" user={data.entry.current_version.author} />
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<PatientCard patient={data.entry.patient} />
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<input type="hidden" name="old_version_id" value={$form.old_version_id} />
|
||||
<input name="old_version_id" type="hidden" value={$form.old_version_id} />
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<FormField label="Kategorie" errors={$errors.category_id}>
|
||||
<FormField errors={$errors.category_id} label="Kategorie">
|
||||
<Autocomplete
|
||||
idInputName="category_id"
|
||||
inputCls="input input-bordered w-full max-w-xs"
|
||||
items={async () => {
|
||||
return await trpc().category.list.query();
|
||||
}}
|
||||
selection={data.entry.current_version.category}
|
||||
items={async () => trpc().category.list.query()}
|
||||
onSelect={(item) => {
|
||||
$form.category_id = item.id;
|
||||
return { newValue: item.name ?? "", close: true };
|
||||
|
@ -57,15 +60,15 @@
|
|||
onUnselect={() => {
|
||||
$form.category_id = null;
|
||||
}}
|
||||
idInputName="category_id"
|
||||
selection={data.entry.current_version.category}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Zu erledigen am" errors={$errors.date}>
|
||||
<FormField errors={$errors.date} label="Zu erledigen am">
|
||||
<input
|
||||
type="date"
|
||||
name="date"
|
||||
aria-invalid={Boolean($errors.date)}
|
||||
type="date"
|
||||
bind:value={$form.date}
|
||||
{...$constraints.date}
|
||||
/>
|
||||
|
@ -75,9 +78,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>
|
||||
|
@ -85,17 +88,17 @@
|
|||
</div>
|
||||
|
||||
<MarkdownInput
|
||||
label="Beschreibung"
|
||||
name="text"
|
||||
marginTop
|
||||
ariaInvalid={Boolean($errors.text)}
|
||||
bind:value={$form.text}
|
||||
errors={$errors.text}
|
||||
label="Beschreibung"
|
||||
marginTop
|
||||
bind:value={$form.text}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="btn btn-primary max-w-32 mt-4"
|
||||
type="submit"
|
||||
disabled={browser && $tainted === undefined}>Speichern</button
|
||||
disabled={browser && $tainted === undefined}
|
||||
type="submit">Speichern</button
|
||||
>
|
||||
</form>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
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) => {
|
||||
|
@ -16,7 +19,7 @@ export const load: PageLoad = async (event) => {
|
|||
old_version_id: entry.current_version.id,
|
||||
...entry.current_version,
|
||||
},
|
||||
SchemaNewEntryVersion
|
||||
SchemaNewEntryVersion,
|
||||
);
|
||||
|
||||
return { entry, form };
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
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 { superValidate } from "sveltekit-superforms";
|
||||
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 = {
|
||||
default: async (event) => {
|
||||
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 { SchemaNewExecution } from "./schema";
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => loadWrap(async () => {
|
||||
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;
|
||||
});
|
||||
|
||||
throw redirect(302, `/entry/${entryId}`);
|
||||
},
|
||||
} satisfies Actions;
|
||||
redirect(302, `/entry/${id}`);
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,30 +1,34 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { PageData } from "./$types";
|
||||
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";
|
||||
import MarkdownInput from "$lib/components/ui/markdown/MarkdownInput.svelte";
|
||||
|
||||
import { SchemaNewExecution } from "./schema";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let formData = defaults(SchemaNewExecution);
|
||||
const formData = defaults(SchemaNewExecution);
|
||||
const { form, errors, enhance } = superForm(formData, {
|
||||
validators: SchemaNewExecution,
|
||||
});
|
||||
</script>
|
||||
|
||||
<EntryBody entry={data.entry} backBtn />
|
||||
<EntryBody backBtn entry={data.entry} />
|
||||
|
||||
<form method="POST" use:enhance>
|
||||
<MarkdownInput
|
||||
label="Ausführungstext bearbeiten"
|
||||
name="text"
|
||||
ariaInvalid={Boolean($errors.text)}
|
||||
bind:value={$form.text}
|
||||
errors={$errors.text}
|
||||
label="Ausführungstext bearbeiten"
|
||||
bind:value={$form.text}
|
||||
>
|
||||
<div class="row c-vlight">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Speichern</button>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Speichern</button>
|
||||
</div>
|
||||
</MarkdownInput>
|
||||
</form>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
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) => {
|
||||
|
@ -16,7 +19,7 @@ export const load: PageLoad = async (event) => {
|
|||
old_execution_id: entry.execution?.id,
|
||||
...entry.execution,
|
||||
},
|
||||
SchemaNewExecution
|
||||
SchemaNewExecution,
|
||||
);
|
||||
|
||||
return { entry, form };
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { ZEntryExecutionNew, fields } from "$lib/shared/model/validation";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
|
||||
import { ZEntryExecutionNew, fields } from "$lib/shared/model/validation";
|
||||
|
||||
export const SchemaNewExecution = zod(
|
||||
ZEntryExecutionNew.extend({
|
||||
old_execution_id: fields.EntityId().optional(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<script lang="ts">
|
||||
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";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import { formatDate } from "$lib/shared/util";
|
||||
|
||||
import UserField from "$lib/components/table/UserField.svelte";
|
||||
import Header from "$lib/components/ui/Header.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
$: entryId = $page.params.id;
|
||||
|
@ -13,7 +15,7 @@
|
|||
<title>Eintrag #{entryId} - Erledigt</title>
|
||||
</svelte:head>
|
||||
|
||||
<Header title="Eintrag #{entryId} - Erledigt" backHref="/entry/{entryId}" />
|
||||
<Header backHref="/entry/{entryId}" title="Eintrag #{entryId} - Erledigt" />
|
||||
|
||||
{#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