Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add alpha DSM code page #148

Merged
merged 5 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import { randomUUID } from "crypto"
import { performance } from "perf_hooks"
import { pool } from "$lib/server/sql"
import { DEVELOPMENT_USER } from "$lib/server/environment"
import type { User } from "$lib/databaseTypes"
import type { User } from "$lib/types"
import type { RequestEvent } from "@sveltejs/kit"

const ADMIN_ENDPOINTS = ["/api/templates/.*?"]
type Endpoint = {
path: string
method: "GET" | "PATCH" | "POST" | "PUT" | "DELETE"
}

const ADMIN_ENDPOINT_PATHS = ["/api/templates/.*?", "/api/dsm/.*?"]
const ADMIN_SPECIFIC_ENDPOINTS: Endpoint[] = [{ path: "/api/dsm", method: "POST" }]

export async function handle({ event, resolve }) {
const requestId = randomUUID()
Expand Down Expand Up @@ -87,8 +93,14 @@ export async function getOrInsertUser(email: string) {
}

function isUserAuthorized(event: RequestEvent<Partial<Record<string, string>>, string | null>, user: User) {
if (!user.is_admin && ADMIN_ENDPOINTS.some(endpoint => event.request.url.match(endpoint))) {
if (!user.is_admin && ADMIN_ENDPOINT_PATHS.some(path => event.request.url.match(path))) {
return false
}

for (const endpoint of ADMIN_SPECIFIC_ENDPOINTS) {
if (!user.is_admin && event.request.url.match(endpoint.path) && event.request.method === endpoint.method) {
return false
}
}
return true
}
1 change: 1 addition & 0 deletions src/lib/components/Navigation.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

const drawerStore = getDrawerStore()
const pages = [
{ name: "DSM Codes", href: "/dsm" },
{ name: "Intake", href: "/intake" },
{ name: "Summarization", href: "/summarization" },
{ name: "Templates", href: "/templates" }
Expand Down
1 change: 1 addition & 0 deletions src/lib/icons/XIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<i class={`fa-solid fa-x ${$$props.class}`}></i>
15 changes: 0 additions & 15 deletions src/lib/markdownRenderers/OrderedListItem.svelte

This file was deleted.

15 changes: 0 additions & 15 deletions src/lib/markdownRenderers/UnorderedListItem.svelte

This file was deleted.

10 changes: 4 additions & 6 deletions src/lib/server/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,14 @@ const config = {

export const pool = new Pool(config)

export interface SqlTemplateModel {
export type SqlTemplateSchema = {
id: number
text: string
parent_id: number | null
time_created: string
time_updated: string
}

export interface SqlTemplateSchema {
export type SqlDsmCodeSchema = {
id: number
text: string
parent_id: number | null
code: string
label: string
}
2 changes: 2 additions & 0 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
} from "@skeletonlabs/skeleton"
import "../app.postcss"
import ModalSearchDecisionTree from "./templates/TemplatesDirectory/ModalSearchDecisionTree.svelte"
import ModalDsmForm from "./dsm/ModalDsmForm.svelte"
initializeStores()
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow })

const modalRegistry: Record<string, ModalComponent> = {
dsmForm: { ref: ModalDsmForm },
markdown: { ref: ModalMarkdown },
searchDecisionTree: { ref: ModalSearchDecisionTree }
}
Expand Down
42 changes: 42 additions & 0 deletions src/routes/api/dsm/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { logger } from "$lib/server/logging"
import { pool, type SqlDsmCodeSchema } from "$lib/server/sql"

export async function GET() {
logger.info("Getting all DSM codes")
return await pool
.connect()
.then(async client => {
const result = await client.query("SELECT * FROM dsm_codes")
client.release()
return result.rows as SqlDsmCodeSchema[]
})
.then(rows => {
return new Response(JSON.stringify(rows), { headers: { "Content-Type": "application/json" } })
})
.catch(error => {
logger.error("Error getting all templates:", error)
return new Response(null, { status: 500 })
})
}

export async function POST({ request }) {
logger.info("Posting a new DSM code")
const { code, label } = await request.json()
return await pool
.connect()
.then(async client => {
const result = await client.query({
text: "INSERT INTO dsm_codes (code, label) VALUES ($1, $2) RETURNING id, code, label",
values: [code, label]
})
client.release()
return result.rows as SqlDsmCodeSchema[]
})
.then(rows => {
return new Response(JSON.stringify(rows[0]), { headers: { "Content-Type": "application/json" } })
})
.catch(error => {
logger.error("Error getting all templates:", error)
return new Response(null, { status: 500 })
})
}
40 changes: 40 additions & 0 deletions src/routes/api/dsm/[id]/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { logger } from "$lib/server/logging"
import { pool, type SqlDsmCodeSchema } from "$lib/server/sql"

export async function PUT({ params, request }) {
const id = params.id
const { code, label } = await request.json()
logger.info("Editting DSM Code")
return await pool
.connect()
.then(async client => {
const result = await client.query({
text: "UPDATE dsm_codes SET code = $1, label = $2 WHERE id = $3",
values: [code, label, id]
})
client.release()
return result.rows as SqlDsmCodeSchema[]
})
.then(rows => {
return new Response(JSON.stringify(rows), { headers: { "Content-Type": "application/json" } })
})
.catch(error => {
logger.error("Error getting all dsm codes:", error)
return new Response(null, { status: 500 })
})
}

export async function DELETE({ params }) {
return await pool
.connect()
.then(async client => {
await client.query({ text: "DELETE FROM dsm_codes WHERE id = $1", values: [params.id] })
})
.then(() => {
return new Response(null)
})
.catch(error => {
logger.error("Error deleting all dsm code:", error)
return new Response(null, { status: 500 })
})
}
136 changes: 136 additions & 0 deletions src/routes/dsm/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<script lang="ts">
import type { SqlDsmCodeSchema } from "$lib/server/sql"
import { getToastStore, type ToastSettings } from "@skeletonlabs/skeleton"
import { onMount } from "svelte"
import EditButton from "./EditButton.svelte"
import CreateButton from "./CreateButton.svelte"
import DeleteButton from "./DeleteButton.svelte"
import { indexForNewItemInSortedList } from "./utils"
import XIcon from "$lib/icons/XIcon.svelte"

export let data

let searchString = ""
let dsmCodes: SqlDsmCodeSchema[] = []
let autoCompeleteOptions: SqlDsmCodeSchema[] = []
let selected: SqlDsmCodeSchema[] = []
let inputDiv: HTMLDivElement
let isAdmin = data.user?.is_admin

const toastStore = getToastStore()
const copyToast: ToastSettings = {
message: "The selected DSM codes have been copied to your clipboard."
}
const noSelectionToast: ToastSettings = {
message: "No DSM codes have been selected.",
background: "variant-filled-error"
}

onMount(() => {
fetch("/api/dsm")
.then(res => res.json())
.then((data: SqlDsmCodeSchema[]) => {
dsmCodes = data.sort((a, b) => a.label.localeCompare(b.label))
})
inputDiv.focus()
})

function onButtonClick(item: { label: string; id: number; code: string }) {
if (selected.some(s => s.label === item.label)) return
selected = [...selected, item]
searchString = ""
inputDiv.focus()
}

function exportToClipboard() {
if (selected.length === 0) {
toastStore.trigger(noSelectionToast)
return
}

function itemToString(item: { label: string; code: string }) {
if (item.code.length < 13) {
return [item.code, item.label].join("\t\t")
} else {
return [item.code, item.label].join("\t")
}
}
navigator.clipboard.writeText(selected.map(s => itemToString(s)).join("\n") + "\n")
toastStore.trigger(copyToast)
}

function onCreate(item: SqlDsmCodeSchema) {
const index = indexForNewItemInSortedList(
dsmCodes.map(d => d.label),
item.label
)
dsmCodes = [...dsmCodes.slice(0, index), item, ...dsmCodes.slice(index)]
}

function onDelete(item: SqlDsmCodeSchema) {
dsmCodes = dsmCodes.filter(code => code.id !== item.id)
}

$: autoCompeleteOptions = dsmCodes.filter(code =>
(code.code + " " + code.label).toLowerCase().includes(searchString.toLowerCase())
)
</script>

<span class="flex space-x-2 pb-2 h-12">
{#if isAdmin}
<CreateButton {onCreate} />
{/if}
</span>

<div class="flex space-x-2">
<input
bind:this={inputDiv}
class="input max-h-10"
type="search"
name="autocomplete-search"
placeholder="Search..."
autocomplete="off"
bind:value={searchString}
/>

<button tabindex="-1" class="btn variant-filled-primary" on:click={exportToClipboard}>
<span>
<i class="fas fa-copy"></i>
Copy
</span>
</button>
</div>

<span class="ml-2 space-x-2 space-y-1">
{#each selected as selection}
<button
class="chip variant-filled hover:variant-filled"
on:click={() => (selected = selected.filter(s => s.id !== selection.id))}
>
<span><XIcon /></span>
<span>{selection.label}</span>
</button>
{/each}
</span>

<div class="max-h-[40vh] p-4 overflow-y-auto border-2 bg-white">
<ul class="w-full">
{#each autoCompeleteOptions as option}
<li class="grid grid-cols-[70px_auto] w-full" class:grid-cols-1={!isAdmin}>
{#if isAdmin}
<span class="grid grid-cols-2 mt-2 gap-3 mr-4">
<EditButton bind:dsmItem={option} />
<DeleteButton dsmItem={option} {onDelete} />
</span>
{/if}
<button
class="btn hover:variant-ghost-primary flex justify-start text-left w-full"
class:variant-soft-primary={selected.some(s => s.label === option.label)}
on:click={() => onButtonClick(option)}
>
{option.label}
</button>
</li>
{/each}
</ul>
</div>
47 changes: 47 additions & 0 deletions src/routes/dsm/CreateButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import FileIcon from "$lib/icons/FileIcon.svelte"
import type { SqlDsmCodeSchema } from "$lib/server/sql"
import { getModalStore, getToastStore, type ModalSettings } from "@skeletonlabs/skeleton"

export let onCreate: (item: SqlDsmCodeSchema) => void

const instructions = "Create a new DSM code."

const toastStore = getToastStore()
const modalStore = getModalStore()

async function onClick() {
const modal: ModalSettings = {
type: "component",
component: "dsmForm",
title: `Create DSM Code`,
meta: { instructions: instructions },
response: async response => {
if (!response) return
await fetch(`/api/dsm`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: response.code, label: response.label })
}).then(async result => {
if (!result.ok) {
toastStore.trigger({
message: `Failed to create the DSM code: ${result.statusText}`,
background: "variant-filled-error"
})
} else {
onCreate(await result.json())
toastStore.trigger({
message: `Created the DSM code.`,
background: "variant-filled-success"
})
}
})
}
}
modalStore.trigger(modal)
}
</script>

<button on:click={onClick} class="btn variant-filled-secondary">
<span>Create DSM Code</span>
</button>
Loading
Loading