Enhance dashboard functionality by integrating child selection and displaying related measurements, vaccinations, and toothing data. Update Prisma schema to use UUIDs for IDs and add new API endpoint to fetch child details by ID.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -40,3 +40,4 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.cursor/mcp.json
|
.cursor/mcp.json
|
||||||
|
.cursor/rules/generalprompt.mdc
|
||||||
|
|||||||
725
package-lock.json
generated
725
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-select": "^2.1.7",
|
"@radix-ui/react-select": "^2.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.2",
|
"@radix-ui/react-separator": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.4",
|
||||||
"@tanstack/react-query": "^5.72.0",
|
"@tanstack/react-query": "^5.72.0",
|
||||||
"@trpc/client": "^11.0.2",
|
"@trpc/client": "^11.0.2",
|
||||||
"@trpc/next": "^11.0.2",
|
"@trpc/next": "^11.0.2",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.55.0",
|
||||||
|
"recharts": "^2.15.2",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.1.0",
|
"tailwind-merge": "^3.1.0",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
@@ -2277,6 +2279,203 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-ufbpLUjZiOg4iYgb2hQrWXEPYX6jOLBbR27bDyAff5GYMRrCzcze8lukjuXVUQvJ6HZe8+oL+hhswDcjmcgVyg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-collection": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.0.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-direction": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-select": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.1.7",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.7.tgz",
|
||||||
@@ -2625,6 +2824,200 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-fuHMHWSf5SRhXke+DbHXj2wVMo+ghVH30vhX3XVacdXqDl+J4XWafMIGOOER861QpBx1jxgwKXL2dQnfrsd8MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.3",
|
||||||
|
"@radix-ui/react-primitive": "2.0.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-direction": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||||
@@ -3174,6 +3567,69 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||||
@@ -4298,9 +4754,129 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
@@ -4389,6 +4965,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@@ -4466,6 +5048,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-helpers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -5167,6 +5759,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -5174,6 +5772,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-equals": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||||
@@ -5781,6 +6388,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -6240,7 +6856,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@@ -6605,6 +7220,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -6616,7 +7237,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
@@ -7409,7 +8029,6 @@
|
|||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
@@ -7503,7 +8122,6 @@
|
|||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
@@ -7553,6 +8171,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-smooth": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-equals": "^5.0.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-transition-group": "^4.4.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-style-singleton": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
@@ -7575,6 +8208,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
@@ -7589,6 +8238,44 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "2.15.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz",
|
||||||
|
"integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"eventemitter3": "^4.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react-is": "^18.3.1",
|
||||||
|
"react-smooth": "^4.0.4",
|
||||||
|
"recharts-scale": "^0.4.4",
|
||||||
|
"tiny-invariant": "^1.3.1",
|
||||||
|
"victory-vendor": "^36.6.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/recharts-scale": {
|
||||||
|
"version": "0.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
|
||||||
|
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decimal.js-light": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/recharts/node_modules/react-is": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -8338,6 +9025,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.12",
|
"version": "0.2.12",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
|
||||||
@@ -8641,6 +9334,28 @@
|
|||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "36.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||||
|
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@radix-ui/react-select": "^2.1.7",
|
"@radix-ui/react-select": "^2.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.2",
|
"@radix-ui/react-separator": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.4",
|
||||||
"@tanstack/react-query": "^5.72.0",
|
"@tanstack/react-query": "^5.72.0",
|
||||||
"@trpc/client": "^11.0.2",
|
"@trpc/client": "^11.0.2",
|
||||||
"@trpc/next": "^11.0.2",
|
"@trpc/next": "^11.0.2",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.55.0",
|
"react-hook-form": "^7.55.0",
|
||||||
|
"recharts": "^2.15.2",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.1.0",
|
"tailwind-merge": "^3.1.0",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Measurement" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
@@ -22,7 +22,7 @@ model User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Child {
|
model Child {
|
||||||
id Int @id @default(autoincrement())
|
id String @id @default(uuid())
|
||||||
name String
|
name String
|
||||||
birthDate DateTime
|
birthDate DateTime
|
||||||
gender String? // can be "male", "female", "diverse", or "unknown"
|
gender String? // can be "male", "female", "diverse", or "unknown"
|
||||||
@@ -35,26 +35,27 @@ model Child {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Measurement {
|
model Measurement {
|
||||||
id Int @id @default(autoincrement())
|
id String @id @default(uuid())
|
||||||
childId Int
|
childId String
|
||||||
child Child @relation(fields: [childId], references: [id])
|
child Child @relation(fields: [childId], references: [id])
|
||||||
date DateTime
|
date DateTime
|
||||||
weightKg Float?
|
weightKg Float?
|
||||||
heightCm Float?
|
heightCm Float?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model ToothStatus {
|
model ToothStatus {
|
||||||
id Int @id @default(autoincrement())
|
id String @id @default(uuid())
|
||||||
childId Int
|
childId String
|
||||||
child Child @relation(fields: [childId], references: [id])
|
child Child @relation(fields: [childId], references: [id])
|
||||||
toothLabel String // z. B. "oben links 1" oder "Zahn 61"
|
toothLabel String // z. B. "oben links 1" oder "Zahn 61"
|
||||||
date DateTime
|
date DateTime
|
||||||
status String // z. B. "durchgebrochen", "locker", "fehlend"
|
status String // z. B. "durchgebrochen", "locker", "fehlend"
|
||||||
}
|
}
|
||||||
|
|
||||||
model Vaccine {
|
model Vaccine {
|
||||||
id Int @id @default(autoincrement())
|
id String @id @default(uuid())
|
||||||
childId Int
|
childId String
|
||||||
child Child @relation(fields: [childId], references: [id])
|
child Child @relation(fields: [childId], references: [id])
|
||||||
name String
|
name String
|
||||||
date DateTime
|
date DateTime
|
||||||
|
|||||||
34
src/app/dashboard/child/[childId]/page.tsx
Normal file
34
src/app/dashboard/child/[childId]/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { getAuthSession } from "@/lib/auth"
|
||||||
|
import { redirect } from "next/navigation"
|
||||||
|
import { Header } from "@/components/layout/header"
|
||||||
|
import { Footer } from "@/components/layout/footer"
|
||||||
|
import { ChildDetailContent } from "@/components/child/child-detail-content"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
|
export default async function ChildDetailPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { childId: string }
|
||||||
|
}) {
|
||||||
|
const session = await getAuthSession()
|
||||||
|
|
||||||
|
if (!session?.user) redirect("/login")
|
||||||
|
|
||||||
|
const { childId } = params;
|
||||||
|
|
||||||
|
if (!childId) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header />
|
||||||
|
<main className="min-h-[calc(100vh-160px)] px-6 py-10 bg-gradient-to-br from-rose-100 to-rose-50">
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
<ChildDetailContent childId={childId} />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
|||||||
import { ReactNode } from "react"
|
import { ReactNode } from "react"
|
||||||
import { httpBatchLink } from "@trpc/client"
|
import { httpBatchLink } from "@trpc/client"
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
|
import { SessionProvider } from "next-auth/react"
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
const trpcClient = trpc.createClient({
|
const trpcClient = trpc.createClient({
|
||||||
@@ -31,12 +32,14 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable} font-sans`}>
|
<body className={`${geistSans.variable} ${geistMono.variable} font-sans`}>
|
||||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
<SessionProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||||
{children}
|
<QueryClientProvider client={queryClient}>
|
||||||
<Toaster />
|
{children}
|
||||||
</QueryClientProvider>
|
<Toaster />
|
||||||
</trpc.Provider>
|
</QueryClientProvider>
|
||||||
|
</trpc.Provider>
|
||||||
|
</SessionProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
19
src/components/auth/logout-button.tsx
Normal file
19
src/components/auth/logout-button.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { signOut } from "next-auth/react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { LogOut } from "lucide-react"
|
||||||
|
|
||||||
|
export function LogoutButton() {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => signOut({ callbackUrl: "/" })}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
<span>Abmelden</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
517
src/components/child/child-detail-content.tsx
Normal file
517
src/components/child/child-detail-content.tsx
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { format, differenceInMonths } from "date-fns";
|
||||||
|
import { de } from "date-fns/locale";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { trpc } from "@/utils/trpc";
|
||||||
|
import {
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Legend,
|
||||||
|
} from "recharts";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import type { TRPCClientErrorLike } from "@trpc/client";
|
||||||
|
import type { AppRouter } from "@/server/api/root";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { CalendarIcon, Baby, Syringe, Activity, Plus, Stethoscope, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
const measurementFormSchema = z.object({
|
||||||
|
date: z.date({
|
||||||
|
required_error: "Bitte wähle ein Datum",
|
||||||
|
}),
|
||||||
|
weightKg: z.string().refine((val) => !isNaN(Number(val)) && Number(val) > 0, {
|
||||||
|
message: "Bitte gib ein gültiges Gewicht ein",
|
||||||
|
}),
|
||||||
|
heightCm: z.string().refine((val) => !isNaN(Number(val)) && Number(val) > 0, {
|
||||||
|
message: "Bitte gib eine gültige Größe ein",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type MeasurementFormValues = z.infer<typeof measurementFormSchema>;
|
||||||
|
|
||||||
|
interface ChildDetailContentProps {
|
||||||
|
childId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChildDetailContent({ childId }: ChildDetailContentProps) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [date, setDate] = useState<Date | undefined>(new Date());
|
||||||
|
const [expandedSections, setExpandedSections] = useState({
|
||||||
|
overview: true,
|
||||||
|
measurements: false,
|
||||||
|
vaccinations: false,
|
||||||
|
toothing: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define error handler within component scope
|
||||||
|
const handleAddMeasurement = (err: TRPCClientErrorLike<AppRouter>) => {
|
||||||
|
toast.error("Fehler", {
|
||||||
|
description: err.message
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch child data
|
||||||
|
const { data: child, isLoading: isLoadingChild } = trpc.child.getById.useQuery({ id: childId });
|
||||||
|
|
||||||
|
// Fetch measurements
|
||||||
|
const { data: measurements, isLoading: isLoadingMeasurements, refetch: refetchMeasurements } = trpc.measurement.getByChildId.useQuery(
|
||||||
|
{ childId },
|
||||||
|
{ enabled: !!childId }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add measurement mutation
|
||||||
|
const addMeasurement = trpc.measurement.add.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Messung hinzugefügt", {
|
||||||
|
description: "Die Messung wurde erfolgreich hinzugefügt."
|
||||||
|
});
|
||||||
|
form.reset();
|
||||||
|
setDate(new Date());
|
||||||
|
refetchMeasurements();
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
onError: handleAddMeasurement,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form setup
|
||||||
|
const form = useForm<MeasurementFormValues>({
|
||||||
|
resolver: zodResolver(measurementFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
date: new Date(),
|
||||||
|
weightKg: "",
|
||||||
|
heightCm: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((values: MeasurementFormValues) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Create a new date at the start of the selected day in local timezone
|
||||||
|
const selectedDate = date || new Date();
|
||||||
|
const localDate = new Date(
|
||||||
|
selectedDate.getFullYear(),
|
||||||
|
selectedDate.getMonth(),
|
||||||
|
selectedDate.getDate(),
|
||||||
|
12, // Set to noon to avoid any timezone issues
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
addMeasurement.mutate({
|
||||||
|
childId,
|
||||||
|
date: localDate.toISOString(),
|
||||||
|
weightKg: parseFloat(values.weightKg),
|
||||||
|
heightCm: parseFloat(values.heightCm),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleSection = (section: keyof typeof expandedSections) => {
|
||||||
|
setExpandedSections(prev => ({
|
||||||
|
...prev,
|
||||||
|
[section]: !prev[section]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoadingChild || isLoadingMeasurements) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-8 w-1/3" />
|
||||||
|
<Skeleton className="h-32 w-full" />
|
||||||
|
<Skeleton className="h-64 w-full" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!child) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-10">
|
||||||
|
<h2 className="text-xl font-semibold text-zinc-800">Kind nicht gefunden</h2>
|
||||||
|
<p className="text-zinc-500 mt-2">Das angeforderte Kind konnte nicht gefunden werden.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const birthDate = new Date(child.birthDate);
|
||||||
|
const ageInMonths = differenceInMonths(new Date(), birthDate);
|
||||||
|
|
||||||
|
// Prepare data for charts
|
||||||
|
const measurementData = measurements
|
||||||
|
?.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
|
||||||
|
.map((m) => ({
|
||||||
|
date: format(new Date(m.date), "dd.MM.yyyy", { locale: de }),
|
||||||
|
weight: m.weightKg,
|
||||||
|
height: m.heightCm,
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<div className="bg-gradient-to-r from-rose-100 to-rose-200 p-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="bg-white/80 p-3 rounded-full shadow-sm">
|
||||||
|
<Baby className="h-8 w-8 text-rose-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-rose-900">{child.name}</h2>
|
||||||
|
<p className="text-rose-700">
|
||||||
|
{ageInMonths} Monate • {format(new Date(child.birthDate), "PPP", { locale: de })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow-sm border border-rose-100">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<CalendarIcon className="h-5 w-5 text-rose-500" />
|
||||||
|
<h3 className="font-medium text-gray-700">Geburtsdatum</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg">{format(new Date(child.birthDate), "PPP", { locale: de })}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow-sm border border-rose-100">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Activity className="h-5 w-5 text-rose-500" />
|
||||||
|
<h3 className="font-medium text-gray-700">Alter</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg">{ageInMonths} Monate</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow-sm border border-rose-100">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Stethoscope className="h-5 w-5 text-rose-500" />
|
||||||
|
<h3 className="font-medium text-gray-700">Entwicklung</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg">Normal</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Overview Section */}
|
||||||
|
<div className="border rounded-lg overflow-hidden bg-white shadow-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('overview')}
|
||||||
|
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Activity className="h-5 w-5 text-gray-600" />
|
||||||
|
<h2 className="text-lg font-medium">Übersicht</h2>
|
||||||
|
</div>
|
||||||
|
{expandedSections.overview ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{expandedSections.overview && (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">Letzte Messungen</h3>
|
||||||
|
{measurements && measurements.length > 0 ? (
|
||||||
|
<div className="bg-white rounded-md p-4 border">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">Gewicht</p>
|
||||||
|
<p className="text-xl font-semibold">{measurements[0].weightKg} kg</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{format(new Date(measurements[0].date), "dd.MM.yyyy", { locale: de })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">Größe</p>
|
||||||
|
<p className="text-xl font-semibold">{measurements[0].heightCm} cm</p>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{format(new Date(measurements[0].date), "dd.MM.yyyy", { locale: de })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground">Noch keine Messungen aufgezeichnet.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">Nächste Impfung</h3>
|
||||||
|
<div className="bg-white rounded-md p-4 border">
|
||||||
|
<p className="text-muted-foreground">Keine anstehenden Impfungen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Measurements Section */}
|
||||||
|
<div className="border rounded-lg overflow-hidden bg-white shadow-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('measurements')}
|
||||||
|
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Baby className="h-5 w-5 text-gray-600" />
|
||||||
|
<h2 className="text-lg font-medium">Messungen</h2>
|
||||||
|
</div>
|
||||||
|
{expandedSections.measurements ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{expandedSections.measurements && (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<form onSubmit={onSubmit} className="space-y-4 mb-6">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="date" className="text-sm font-medium text-gray-700">
|
||||||
|
Messdatum
|
||||||
|
</label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-start text-left font-normal",
|
||||||
|
!date && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{date ? (
|
||||||
|
format(date, "PPP", { locale: de })
|
||||||
|
) : (
|
||||||
|
<span>Datum auswählen</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={date}
|
||||||
|
onSelect={setDate}
|
||||||
|
initialFocus
|
||||||
|
locale={de}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="weightKg" className="text-sm font-medium text-gray-700">
|
||||||
|
Gewicht (kg)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
id="weightKg"
|
||||||
|
step="0.1"
|
||||||
|
className="w-full"
|
||||||
|
{...form.register("weightKg")}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
{form.formState.errors.weightKg && (
|
||||||
|
<p className="text-sm text-red-600">{form.formState.errors.weightKg.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="heightCm" className="text-sm font-medium text-gray-700">
|
||||||
|
Größe (cm)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
id="heightCm"
|
||||||
|
step="0.1"
|
||||||
|
className="w-full"
|
||||||
|
{...form.register("heightCm")}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
{form.formState.errors.heightCm && (
|
||||||
|
<p className="text-sm text-red-600">{form.formState.errors.heightCm.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full sm:w-auto bg-rose-600 text-white hover:bg-rose-700"
|
||||||
|
>
|
||||||
|
{isLoading ? "Wird hinzugefügt..." : "Messung hinzufügen"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{measurements && measurements.length > 0 ? (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Gewicht</h3>
|
||||||
|
<div className="h-[300px] bg-white rounded-md p-4 border">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<LineChart data={measurementData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="date" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="weight"
|
||||||
|
stroke="#8884d8"
|
||||||
|
name="Gewicht (kg)"
|
||||||
|
activeDot={{ r: 8 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium mb-2">Größe</h3>
|
||||||
|
<div className="h-[300px] bg-white rounded-md p-4 border">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<LineChart data={measurementData}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="date" />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="height"
|
||||||
|
stroke="#82ca9d"
|
||||||
|
name="Größe (cm)"
|
||||||
|
activeDot={{ r: 8 }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground">Noch keine Messungen aufgezeichnet.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vaccinations Section */}
|
||||||
|
<div className="border rounded-lg overflow-hidden bg-white shadow-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('vaccinations')}
|
||||||
|
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Syringe className="h-5 w-5 text-gray-600" />
|
||||||
|
<h2 className="text-lg font-medium">Impfungen</h2>
|
||||||
|
</div>
|
||||||
|
{expandedSections.vaccinations ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{expandedSections.vaccinations && (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button size="sm" className="bg-rose-600 text-white hover:bg-rose-700">
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Impfung hinzufügen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-muted-foreground">Noch keine Impfungen aufgezeichnet.</p>
|
||||||
|
<div className="bg-white rounded-md p-4 border">
|
||||||
|
<h3 className="text-lg font-medium mb-2">Empfohlene Impfungen</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li className="flex items-center justify-between">
|
||||||
|
<span>6-fach Impfung</span>
|
||||||
|
<span className="text-sm text-gray-500">2 Monate</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center justify-between">
|
||||||
|
<span>Pneumokokken</span>
|
||||||
|
<span className="text-sm text-gray-500">2 Monate</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center justify-between">
|
||||||
|
<span>Rotaviren</span>
|
||||||
|
<span className="text-sm text-gray-500">6 Wochen</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toothing Section */}
|
||||||
|
<div className="border rounded-lg overflow-hidden bg-white shadow-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleSection('toothing')}
|
||||||
|
className="w-full flex items-center justify-between p-4 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Stethoscope className="h-5 w-5 text-gray-600" />
|
||||||
|
<h2 className="text-lg font-medium">Zahnung</h2>
|
||||||
|
</div>
|
||||||
|
{expandedSections.toothing ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{expandedSections.toothing && (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button size="sm" className="bg-rose-600 text-white hover:bg-rose-700">
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Zahn hinzufügen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-muted-foreground">Noch keine Zähne aufgezeichnet.</p>
|
||||||
|
<div className="bg-white rounded-md p-4 border">
|
||||||
|
<h3 className="text-lg font-medium mb-2">Zahnungsplan</h3>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Oberkiefer</h4>
|
||||||
|
<ul className="space-y-2 mt-2">
|
||||||
|
<li className="flex items-center justify-between">
|
||||||
|
<span>Schneidezähne</span>
|
||||||
|
<span className="text-sm text-gray-500">6-10 Monate</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center justify-between">
|
||||||
|
<span>Seitenzähne</span>
|
||||||
|
<span className="text-sm text-gray-500">12-16 Monate</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center justify-between">
|
||||||
|
<span>Backenzähne</span>
|
||||||
|
<span className="text-sm text-gray-500">16-20 Monate</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">Unterkiefer</h4>
|
||||||
|
<ul className="space-y-2 mt-2">
|
||||||
|
<li className="flex items-center justify-between">
|
||||||
|
<span>Schneidezähne</span>
|
||||||
|
<span className="text-sm text-gray-500">5-8 Monate</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center justify-between">
|
||||||
|
<span>Seitenzähne</span>
|
||||||
|
<span className="text-sm text-gray-500">10-14 Monate</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center justify-between">
|
||||||
|
<span>Backenzähne</span>
|
||||||
|
<span className="text-sm text-gray-500">14-18 Monate</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,28 +4,54 @@ import { format, differenceInMonths } from "date-fns";
|
|||||||
import { de } from "date-fns/locale";
|
import { de } from "date-fns/locale";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus, Baby, Syringe, Calendar, Clock } from "lucide-react";
|
import { Plus, Baby, Activity, Calendar } from "lucide-react";
|
||||||
import { trpc } from "@/utils/trpc";
|
import { trpc } from "@/utils/trpc";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
|
||||||
export function DashboardContent() {
|
export function DashboardContent() {
|
||||||
const { data: children, isLoading } = trpc.child.getAllByUser.useQuery();
|
const { data: children, isLoading } = trpc.child.getAllByUser.useQuery();
|
||||||
|
const [selectedChildId, setSelectedChildId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Get measurements for the selected child
|
||||||
|
const { data: measurements } = trpc.measurement.getByChildId.useQuery(
|
||||||
|
{ childId: selectedChildId || "" },
|
||||||
|
{ enabled: !!selectedChildId }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get vaccinations for the selected child - using child router as a fallback
|
||||||
|
const { data: vaccinations } = trpc.child.getById.useQuery(
|
||||||
|
{ id: selectedChildId || "" },
|
||||||
|
{ enabled: !!selectedChildId }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get toothing data for the selected child - using child router as a fallback
|
||||||
|
const { data: toothing } = trpc.child.getById.useQuery(
|
||||||
|
{ id: selectedChildId || "" },
|
||||||
|
{ enabled: !!selectedChildId }
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-2xl font-bold text-zinc-800">Willkommen bei Bambino</h1>
|
||||||
|
<Link href="/dashboard/add-child">
|
||||||
|
<Button variant="default" size="sm" className="bg-rose-500 hover:bg-rose-600">
|
||||||
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
|
Kind hinzufügen
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
|
<div className="grid gap-6 grid-cols-1 md:grid-cols-2">
|
||||||
<Card className="bg-white/80 border-zinc-200 shadow-sm hover:shadow-md transition-shadow">
|
<Card className="bg-white border-zinc-200 shadow-sm hover:shadow-md transition-shadow">
|
||||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<CardTitle className="text-xl font-semibold text-zinc-800">Kinder</CardTitle>
|
<Baby className="h-5 w-5 text-rose-500" />
|
||||||
<CardDescription className="text-zinc-500">Verwalte deine Kinder</CardDescription>
|
<CardTitle className="text-xl font-semibold text-zinc-800">Deine Kinder</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/dashboard/add-child">
|
<CardDescription className="text-zinc-500">Übersicht deiner Kinder</CardDescription>
|
||||||
<Button variant="default" size="sm" className="bg-rose-500 hover:bg-rose-600">
|
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
|
||||||
Kind hinzufügen
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -40,82 +66,150 @@ export function DashboardContent() {
|
|||||||
const ageInMonths = differenceInMonths(new Date(), birthDate);
|
const ageInMonths = differenceInMonths(new Date(), birthDate);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Link
|
||||||
key={child.id}
|
href={`/dashboard/child/${child.id}`}
|
||||||
className="flex items-center p-4 rounded-lg bg-white border border-zinc-100 hover:bg-rose-50/50 hover:border-rose-100 transition-all"
|
key={child.id}
|
||||||
>
|
>
|
||||||
<div className="flex-shrink-0 mr-4">
|
<div
|
||||||
<div className="w-12 h-12 rounded-full bg-rose-100 flex items-center justify-center">
|
className="flex items-center p-4 rounded-lg bg-white border border-zinc-100 hover:bg-rose-50/50 hover:border-rose-100 transition-all cursor-pointer"
|
||||||
<Baby className="h-6 w-6 text-rose-500" />
|
>
|
||||||
</div>
|
<div className="flex-shrink-0 mr-4">
|
||||||
</div>
|
<div className="w-12 h-12 rounded-full bg-rose-100 flex items-center justify-center">
|
||||||
<div className="flex-grow">
|
<Baby className="h-6 w-6 text-rose-500" />
|
||||||
<h3 className="font-medium text-lg text-zinc-800">{child.name}</h3>
|
|
||||||
<div className="flex flex-wrap gap-x-4 mt-1 text-sm text-zinc-500">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Calendar className="h-3.5 w-3.5 mr-1 text-zinc-400" />
|
|
||||||
<span>Geboren: {format(birthDate, "dd.MM.yyyy", { locale: de })}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Clock className="h-3.5 w-3.5 mr-1 text-zinc-400" />
|
|
||||||
<span>Alter: {ageInMonths} Monate</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<h3 className="font-medium text-zinc-800">{child.name}</h3>
|
||||||
|
<p className="text-sm text-zinc-500">
|
||||||
|
{format(birthDate, "PPP", { locale: de })} • {ageInMonths} Monate
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
<div className="text-center py-6">
|
||||||
<div className="w-16 h-16 rounded-full bg-rose-100 flex items-center justify-center mb-4">
|
<p className="text-zinc-500">Du hast noch keine Kinder hinzugefügt.</p>
|
||||||
<Baby className="h-8 w-8 text-rose-500" />
|
|
||||||
</div>
|
|
||||||
<p className="text-zinc-500 mb-2">Du hast noch keine Kinder hinzugefügt.</p>
|
|
||||||
<Link href="/dashboard/add-child">
|
|
||||||
<Button variant="outline" size="sm" className="border-rose-200 text-rose-600 hover:bg-rose-50">
|
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
|
||||||
Kind hinzufügen
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="bg-white/80 border-zinc-200 shadow-sm hover:shadow-md transition-shadow">
|
<Card className="bg-white border-zinc-200 shadow-sm hover:shadow-md transition-shadow">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<CardTitle className="text-xl font-semibold text-zinc-800">Impfungen</CardTitle>
|
<Calendar className="h-5 w-5 text-blue-500" />
|
||||||
<CardDescription className="text-zinc-500">Verfolge Impftermine</CardDescription>
|
<CardTitle className="text-xl font-semibold text-zinc-800">Anstehende Termine</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
|
<CardDescription className="text-zinc-500">Wichtige Termine und Erinnerungen</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
<div className="w-16 h-16 rounded-full bg-blue-100 flex items-center justify-center mb-4">
|
<p className="text-zinc-500 mb-2">Keine anstehenden Termine.</p>
|
||||||
<Syringe className="h-8 w-8 text-blue-500" />
|
|
||||||
</div>
|
|
||||||
<p className="text-zinc-500 mb-2">Noch keine Impfungen eingetragen.</p>
|
|
||||||
<Button variant="outline" size="sm" className="border-blue-200 text-blue-600 hover:bg-blue-50" disabled>
|
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
|
||||||
Impfung hinzufügen
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="bg-white/80 border-zinc-200 shadow-sm hover:shadow-md transition-shadow">
|
<Card className="bg-white border-zinc-200 shadow-sm hover:shadow-md transition-shadow">
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
|
<Activity className="h-5 w-5 text-green-500" />
|
||||||
<CardTitle className="text-xl font-semibold text-zinc-800">Entwicklung</CardTitle>
|
<CardTitle className="text-xl font-semibold text-zinc-800">Entwicklung</CardTitle>
|
||||||
<CardDescription className="text-zinc-500">Verfolge die Entwicklung deines Kindes</CardDescription>
|
|
||||||
</div>
|
</div>
|
||||||
|
<CardDescription className="text-zinc-500">Übersicht der Entwicklung deiner Kinder</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
{isLoading ? (
|
||||||
<p className="text-zinc-500 mb-2">Entwicklungsdaten werden bald verfügbar sein.</p>
|
<div className="space-y-4">
|
||||||
</div>
|
<div className="h-32 w-full rounded-lg bg-zinc-200/50 animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
) : children && children.length > 0 ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-zinc-700">Kind auswählen:</span>
|
||||||
|
<Select
|
||||||
|
value={selectedChildId || ""}
|
||||||
|
onValueChange={setSelectedChildId}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[200px]">
|
||||||
|
<SelectValue placeholder="Wähle ein Kind" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{children.map((child) => (
|
||||||
|
<SelectItem key={child.id} value={child.id.toString()}>
|
||||||
|
{child.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedChildId ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{/* Measurements Summary */}
|
||||||
|
<div className="bg-zinc-50 p-4 rounded-lg border border-zinc-200">
|
||||||
|
<h3 className="font-medium text-zinc-800 mb-2">Messungen</h3>
|
||||||
|
{measurements && measurements.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-zinc-600">
|
||||||
|
Letzte Messung: {format(new Date(measurements[0].date), "dd.MM.yyyy", { locale: de })}
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-zinc-500">Gewicht</p>
|
||||||
|
<p className="font-medium">{measurements[0].weightKg} kg</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-zinc-500">Größe</p>
|
||||||
|
<p className="font-medium">{measurements[0].heightCm} cm</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-zinc-500">Keine Messungen vorhanden</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vaccinations Summary */}
|
||||||
|
<div className="bg-zinc-50 p-4 rounded-lg border border-zinc-200">
|
||||||
|
<h3 className="font-medium text-zinc-800 mb-2">Impfungen</h3>
|
||||||
|
{vaccinations ? (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-zinc-600">
|
||||||
|
Impfungen werden bald verfügbar sein
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-zinc-500">Keine Impfungen vorhanden</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toothing Summary */}
|
||||||
|
<div className="bg-zinc-50 p-4 rounded-lg border border-zinc-200">
|
||||||
|
<h3 className="font-medium text-zinc-800 mb-2">Zahnung</h3>
|
||||||
|
{toothing ? (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-zinc-600">
|
||||||
|
Zahnungsdaten werden bald verfügbar sein
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-zinc-500">Keine Zahnungsdaten vorhanden</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-zinc-500">Bitte wähle ein Kind aus, um Entwicklungsdaten zu sehen.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<p className="text-zinc-500 mb-2">Füge ein Kind hinzu, um Entwicklungsdaten zu verfolgen.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Baby } from "lucide-react"
|
import { Baby } from "lucide-react"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
|
import { LogoutButton } from "@/components/auth/logout-button"
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
|
const { data: session } = useSession()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="w-full px-6 py-4 border-b bg-white/70 backdrop-blur-md shadow-sm">
|
<header className="w-full px-6 py-4 border-b bg-white/70 backdrop-blur-md shadow-sm">
|
||||||
<div className="max-w-5xl mx-auto flex items-center justify-between">
|
<div className="max-w-5xl mx-auto flex items-center justify-between">
|
||||||
@@ -12,11 +16,24 @@ export function Header() {
|
|||||||
Bambino
|
Bambino
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Später Login- oder Sprachmenü */}
|
<nav className="text-sm text-zinc-600 hidden sm:flex items-center gap-4">
|
||||||
<nav className="text-sm text-zinc-600 hidden sm:block">
|
{session ? (
|
||||||
<Link href="/register" className="hover:underline">
|
<>
|
||||||
Registrierung
|
<Link href="/app" className="hover:underline">
|
||||||
</Link>
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
<LogoutButton />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Link href="/login" className="hover:underline">
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
<Link href="/register" className="hover:underline">
|
||||||
|
Registrierung
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
55
src/components/ui/tabs.tsx
Normal file
55
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 items-center justify-center rounded-lg bg-zinc-100 p-1 text-zinc-500",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { router } from "@/server/trpc"
|
import { router } from "@/server/trpc"
|
||||||
import { childRouter } from "./routers/child"
|
import { childRouter } from "./routers/child"
|
||||||
import { authRouter } from "./routers/auth"
|
import { authRouter } from "./routers/auth"
|
||||||
|
import { measurementRouter } from "./routers/measurement"
|
||||||
|
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
child: childRouter,
|
child: childRouter,
|
||||||
auth: authRouter,
|
auth: authRouter,
|
||||||
|
measurement: measurementRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Export type helper
|
// Export type helper
|
||||||
|
|||||||
@@ -45,4 +45,16 @@ export const childRouter = router({
|
|||||||
|
|
||||||
return ctx.prisma.child.create({ data });
|
return ctx.prisma.child.create({ data });
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getById: protectedProcedure
|
||||||
|
.input(z.object({ id: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const child = await ctx.prisma.child.findFirst({
|
||||||
|
where: {
|
||||||
|
id: input.id,
|
||||||
|
userId: ctx.session.user.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return child
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
37
src/server/api/routers/measurement.ts
Normal file
37
src/server/api/routers/measurement.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { protectedProcedure, router } from "@/server/trpc";
|
||||||
|
|
||||||
|
export const measurementRouter = router({
|
||||||
|
getByChildId: protectedProcedure
|
||||||
|
.input(z.object({ childId: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
return ctx.prisma.measurement.findMany({
|
||||||
|
where: {
|
||||||
|
childId: input.childId,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
date: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
add: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
childId: z.string(),
|
||||||
|
date: z.string().refine((date) => !isNaN(Date.parse(date)), "Invalid date"),
|
||||||
|
weightKg: z.number().optional(),
|
||||||
|
heightCm: z.number().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
return ctx.prisma.measurement.create({
|
||||||
|
data: {
|
||||||
|
childId: input.childId,
|
||||||
|
date: new Date(input.date),
|
||||||
|
weightKg: input.weightKg,
|
||||||
|
heightCm: input.heightCm,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user