diff --git a/bun.lock b/bun.lock index 0501fb1..8b4a478 100644 --- a/bun.lock +++ b/bun.lock @@ -10,9 +10,11 @@ "bun-plugin-tailwind": "^0.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "kysely": "^0.28.8", "lucide-react": "^0.545.0", "react": "^19", "react-dom": "^19", + "sqlite": "^5.1.1", "tailwind-merge": "^3.3.1", }, "devDependencies": { @@ -135,6 +137,8 @@ "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + "kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="], + "lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="], "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], @@ -149,6 +153,8 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "sqlite": ["sqlite@5.1.1", "", {}, "sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], diff --git a/flow.db b/flow.db new file mode 100644 index 0000000..d74ef36 Binary files /dev/null and b/flow.db differ diff --git a/package.json b/package.json index 1082313..fa6c110 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,11 @@ "bun-plugin-tailwind": "^0.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "kysely": "^0.28.8", "lucide-react": "^0.545.0", "react": "^19", "react-dom": "^19", + "sqlite": "^5.1.1", "tailwind-merge": "^3.3.1" }, "devDependencies": { diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..94f15e9 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,179 @@ +// db.ts +import { Database } from "bun:sqlite"; + +// --- Initialize Bun SQLite --- +const db = new Database("./flow.db"); + +// --- Helper functions to bypass TS binding issues --- +function getOne(sql: string, ...params: any[]): T | null { + const row = db.query(sql).get(...params as any); + return row ? (row as T) : null; +} + +function getAll(sql: string, ...params: any[]): T[] { + const rows = db.query(sql).all(...params as any); + return rows.map(r => r as T); +} + +function run(sql: string, ...params: any[]) { + db.run(sql, ...params as any); +} + +// --- Create tables if they do not exist --- +run(` +CREATE TABLE IF NOT EXISTS flow_steps ( + id TEXT PRIMARY KEY, + stepNumber TEXT, + caseCode TEXT, + subStep TEXT, + stage TEXT, + role TEXT, + handoffTo TEXT, + handoffChannel TEXT, + description TEXT, + createdAt TEXT DEFAULT CURRENT_TIMESTAMP +) +`); + +run(` +CREATE TABLE IF NOT EXISTS extended_descriptions ( + stepId TEXT PRIMARY KEY REFERENCES flow_steps(id), + content TEXT +) +`); + +run(` +CREATE TABLE IF NOT EXISTS questions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + stepId TEXT REFERENCES flow_steps(id), + question TEXT, + followUp TEXT, + createdAt TEXT DEFAULT CURRENT_TIMESTAMP +) +`); + +run(` +CREATE TABLE IF NOT EXISTS concerns ( + stepId TEXT PRIMARY KEY REFERENCES flow_steps(id), + flagged INTEGER DEFAULT 0, + comment TEXT, + createdAt TEXT DEFAULT CURRENT_TIMESTAMP +) +`); + +// --- Types --- +export type FlowStep = { + id: string; + stepNumber: string; + caseCode: string; + subStep: string; + stage: string; + role: string; + handoffTo: string; + handoffChannel: string; + description: string; + createdAt: string; +}; + +export type ExtendedDescription = { + stepId: string; + content: string; +}; + +export type Question = { + id: number; + stepId: string; + question: string; + followUp: string | null; + createdAt: string; +}; + +export type Concern = { + stepId: string; + flagged: boolean; + comment: string; + createdAt: string; +}; + +// --- Flow Steps --- +export function createStep(step: Omit) { + run( + `INSERT OR IGNORE INTO flow_steps + (id, stepNumber, caseCode, subStep, stage, role, handoffTo, handoffChannel, description) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + step.id, + step.stepNumber, + step.caseCode, + step.subStep, + step.stage, + step.role, + step.handoffTo, + step.handoffChannel, + step.description + ); +} + +export function getStep(id: string): FlowStep | null { + return getOne("SELECT * FROM flow_steps WHERE id = ?", id); +} + +export function getAllSteps(): FlowStep[] { + return getAll("SELECT * FROM flow_steps"); +} + +// --- Extended Descriptions --- +export function saveExtendedDescription(stepId: string, content: string) { + run( + `INSERT OR REPLACE INTO extended_descriptions (stepId, content) VALUES (?, ?)`, + stepId, + content + ); +} + +export function getExtendedDescription(stepId: string): string | null { + const row = getOne<{ content: string }>( + "SELECT content FROM extended_descriptions WHERE stepId = ?", + stepId + ); + return row ? row.content : null; +} + +// --- Questions --- +export function addQuestion(stepId: string, question: string, followUp?: string | null) { + run( + `INSERT INTO questions (stepId, question, followUp) VALUES (?, ?, ?)`, + stepId, + question, + followUp ?? null + ); +} + +export function getQuestions(stepId: string): Question[] { + return getAll("SELECT * FROM questions WHERE stepId = ?", stepId); +} + +// --- Concerns --- +export function flagConcern(stepId: string, flagged: boolean, comment: string) { + run( + `INSERT OR REPLACE INTO concerns (stepId, flagged, comment) VALUES (?, ?, ?)`, + stepId, + flagged ? 1 : 0, + comment + ); +} + +export function getConcern(stepId: string): Concern | null { + const row = getOne<{ stepId: string; flagged: number; comment: string; createdAt: string }>( + "SELECT * FROM concerns WHERE stepId = ?", + stepId + ); + if (!row) return null; + return { + stepId: row.stepId, + flagged: row.flagged === 1, + comment: row.comment, + createdAt: row.createdAt + }; +} + +//export default db; diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..58c7820 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,105 @@ +// src/server.ts +import { serve } from "bun"; +import { + createStep, + getStep, + getAllSteps, + saveExtendedDescription, + getExtendedDescription, + addQuestion, + getQuestions, + flagConcern, + getConcern +} from "./db.ts"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", +}; + +async function getJSON(req: Request) { + try { + return await req.json(); + } catch { + return {}; + } +} + +serve({ + port: 3000, + fetch: async (req: Request) => { + const { method } = req; + const path = new URL(req.url).pathname; + + if (method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + // --- Steps --- + if (path === "/steps" && method === "GET") { + return new Response(JSON.stringify(getAllSteps()), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); + } + + if (path.startsWith("/steps/") && method === "GET") { + const stepId = String(path.split("/")[2]); + return new Response(JSON.stringify(getStep(stepId)), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); + } + + if (path === "/steps" && method === "POST") { + const data = await getJSON(req); + createStep({ + id: String(data.id), + stepNumber: String(data.stepNumber), + caseCode: String(data.caseCode), + subStep: String(data.subStep), + stage: String(data.stage), + role: String(data.role), + handoffTo: String(data.handoffTo), + handoffChannel: String(data.handoffChannel), + description: String(data.description), + }); + return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); + } + + // --- Extended Descriptions --- + if (path === "/descriptions" && method === "POST") { + const { stepId, content } = await getJSON(req); + saveExtendedDescription(String(stepId), String(content)); + return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); + } + + if (path.startsWith("/descriptions/") && method === "GET") { + const stepId = String(path.split("/")[2]); + return new Response(JSON.stringify({ stepId, content: getExtendedDescription(stepId) }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); + } + + // --- Questions --- + if (path === "/questions" && method === "POST") { + const { stepId, question, followUp } = await getJSON(req); + addQuestion(String(stepId), String(question), followUp ? String(followUp) : null); + return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); + } + + if (path.startsWith("/questions/") && method === "GET") { + const stepId = String(path.split("/")[2]); + return new Response(JSON.stringify(getQuestions(stepId)), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); + } + + // --- Concerns --- + if (path === "/concerns" && method === "POST") { + const { stepId, flagged, comment } = await getJSON(req); + flagConcern(String(stepId), Boolean(flagged), String(comment)); + return new Response(JSON.stringify({ success: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); + } + + if (path.startsWith("/concerns/") && method === "GET") { + const stepId = String(path.split("/")[2]); + return new Response(JSON.stringify(getConcern(stepId)), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); + } + + return new Response("Not Found", { status: 404 }); + } +}); + +console.log("Server running at http://localhost:3000"); diff --git a/src/workflow/parser.ts b/src/workflow/parser.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/workflow/renderer.ts b/src/workflow/renderer.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/workflow/test.js b/src/workflow/test.js new file mode 100644 index 0000000..486ecf9 --- /dev/null +++ b/src/workflow/test.js @@ -0,0 +1,81 @@ +// test-server.ts +async function test() { + const base = "http://localhost:3000"; + + // --- Create a step --- + const stepData = { + id: "step1", + stepNumber: "0.1", + caseCode: "caseA", + subStep: "a", + stage: "Pre-Intake", + role: "Estimator", + handoffTo: "Office Manager", + handoffChannel: "Phone", + description: "Initial ITB received", + }; + + console.log("Creating step..."); + let res = await fetch(`${base}/steps`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(stepData), + }); + console.log(await res.json()); + + // --- Get the step --- + console.log("Getting step..."); + res = await fetch(`${base}/steps/${stepData.id}`); + console.log(await res.json()); + + // --- Add extended description --- + console.log("Adding extended description..."); + res = await fetch(`${base}/descriptions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ stepId: stepData.id, content: "Extended info about step1" }), + }); + console.log(await res.json()); + + // --- Get extended description --- + console.log("Getting extended description..."); + res = await fetch(`${base}/descriptions/${stepData.id}`); + console.log(await res.json()); + + // --- Add a question --- + console.log("Adding question..."); + res = await fetch(`${base}/questions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ stepId: stepData.id, question: "Follow up question?", followUp: "Yes, clarify details" }), + }); + console.log(await res.json()); + + // --- Get questions --- + console.log("Getting questions..."); + res = await fetch(`${base}/questions/${stepData.id}`); + console.log(await res.json()); + + // --- Add a concern --- + console.log("Adding concern..."); + res = await fetch(`${base}/concerns`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ stepId: stepData.id, flagged: true, comment: "Check this step for errors" }), + }); + console.log(await res.json()); + + // --- Get concern --- + console.log("Getting concern..."); + res = await fetch(`${base}/concerns/${stepData.id}`); + console.log(await res.json()); + + // --- Get all steps --- + console.log("Getting all steps..."); + res = await fetch(`${base}/steps`); + console.log(await res.json()); + + console.log("Test complete."); +} + +test(); diff --git a/src/workflow/useWorkflow.ts b/src/workflow/useWorkflow.ts new file mode 100644 index 0000000..abc909c --- /dev/null +++ b/src/workflow/useWorkflow.ts @@ -0,0 +1,5 @@ +import workflow from "./workflow.json"; + +export function useWorkflow() { + return workflow; +} \ No newline at end of file diff --git a/src/workflow/workflow.json b/src/workflow/workflow.json new file mode 100644 index 0000000..e69de29 diff --git a/src/workflow/workflow.types.ts b/src/workflow/workflow.types.ts new file mode 100644 index 0000000..e69de29 diff --git a/test.db b/test.db new file mode 100644 index 0000000..4e99df9 Binary files /dev/null and b/test.db differ