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