initial commit
This commit is contained in:
commit
3315375382
|
|
@ -0,0 +1,10 @@
|
|||
inventory.sqlite
|
||||
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
node_modules
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="inventory.sqlite" uuid="93ba97d4-234b-414f-883d-75f31320c1fa">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:inventory.sqlite</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<includedPredefinedLibrary name="Node.js Core" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/stockkeeping.iml" filepath="$PROJECT_DIR$/.idea/stockkeeping.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="PROJECT" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions" suppressed-tasks="SCSS" />
|
||||
</project>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,10 @@
|
|||
approvedGitRepositories:
|
||||
- "**"
|
||||
|
||||
enableScripts: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 0
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.18.0.cjs
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"packageManager": "yarn@4.18.0",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^2.1.5",
|
||||
"@types/node": "^26.2.0",
|
||||
"hono": "^4.13.16",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"sass": "^1.107.1",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.3.5"
|
||||
},
|
||||
"scripts": {
|
||||
"fe:dev": "vite",
|
||||
"fe:build": "vite build",
|
||||
"be:dev": "tsx watch src/main.ts"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
export interface Location {
|
||||
id: number, // integer
|
||||
cabinet: string | null,
|
||||
drawer: string | null,
|
||||
bin: string | null,
|
||||
}
|
||||
export const locationColumns = [
|
||||
// id omitted on purpose
|
||||
"cabinet",
|
||||
"drawer",
|
||||
"bin",
|
||||
] as const satisfies (keyof Location)[];
|
||||
|
||||
export interface Component {
|
||||
id: number, // integer
|
||||
partNumber: string,
|
||||
manufacturer: string | null,
|
||||
description: string | null,
|
||||
packaging: string | null,
|
||||
category: string | null,
|
||||
package: string | null,
|
||||
value: string | null,
|
||||
quantity: number, // integer
|
||||
unit: string,
|
||||
location: number | null, // integer
|
||||
datasheet: string | null,
|
||||
}
|
||||
export const componentColumns = [
|
||||
// id omitted on purpose
|
||||
"partNumber",
|
||||
"manufacturer",
|
||||
"description",
|
||||
"packaging",
|
||||
"category",
|
||||
"package",
|
||||
"value",
|
||||
"quantity",
|
||||
"unit",
|
||||
"location",
|
||||
"datasheet",
|
||||
] as const satisfies (keyof Component)[];
|
||||
|
||||
export interface Project {
|
||||
id: number, // integer
|
||||
name: string,
|
||||
}
|
||||
export const projectColumns = [
|
||||
"name",
|
||||
] as const satisfies (keyof Project)[];
|
||||
|
||||
export interface ProjectComponent {
|
||||
project: number, // integer
|
||||
component: number, // integer
|
||||
quantity: number, // integer
|
||||
}
|
||||
export const projectComponentColumns = [
|
||||
"project",
|
||||
"component",
|
||||
"quantity",
|
||||
] as const satisfies (keyof ProjectComponent)[];
|
||||
|
||||
export interface Movement {
|
||||
id: number, // integer
|
||||
component: number, // integer
|
||||
quantity: number, // integer
|
||||
date: number | null, // integer
|
||||
reason: string | null,
|
||||
project: number | null, // integer
|
||||
price: number | null, // integer
|
||||
}
|
||||
export const movementColumns = [
|
||||
"component",
|
||||
"quantity",
|
||||
"date",
|
||||
"reason",
|
||||
"project",
|
||||
"price",
|
||||
] as const satisfies (keyof Movement)[];
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
import { showRoutes } from "hono/dev";
|
||||
import { DatabaseSync, SQLInputValue } from "node:sqlite";
|
||||
import { componentColumns, locationColumns, movementColumns, projectColumns, projectComponentColumns } from "../shared/types";
|
||||
|
||||
const app = new Hono();
|
||||
const api = new Hono().basePath("/api");
|
||||
|
||||
const db = new DatabaseSync("./inventory.sqlite");
|
||||
const sql = db.createTagStore();
|
||||
sql.run`PRAGMA foreign_keys = ON`;
|
||||
|
||||
const wrapWithTransaction = <T>(runnable: () => T): T => {
|
||||
try {
|
||||
sql.run`BEGIN`;
|
||||
const result = runnable();
|
||||
sql.run`COMMIT`;
|
||||
return result;
|
||||
} catch (e) {
|
||||
sql.run`ROLLBACK`;
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
const insertHelper = <K extends PropertyKey>(table: string, columns: K[], data: Record<K, SQLInputValue>) => {
|
||||
return db.prepare(`
|
||||
INSERT INTO ${table} (${columns.join(", ")})
|
||||
VALUES (${columns.map(() => "?").join(", ")})
|
||||
`).run(...columns.map(k => data[k] ?? null));
|
||||
};
|
||||
const patchHelper = <K extends PropertyKey>(table: string, columns: K[], id: number, data: Partial<Record<K, SQLInputValue>>) => {
|
||||
const validEntries = Object.entries(data).filter(([k]) => columns.includes(k as K));
|
||||
if (validEntries.length === 0) throw new Error("Nothing to update");
|
||||
return db.prepare(`
|
||||
UPDATE ${table}
|
||||
SET ${validEntries.map(([k]) => `${k} = ?`).join(", ")}
|
||||
WHERE id = ?
|
||||
`).run(...validEntries.map(([, v]) => v as any), id);
|
||||
};
|
||||
|
||||
const tables: [string, string[]][] = [
|
||||
[ "location", locationColumns ],
|
||||
[ "component", componentColumns ],
|
||||
[ "project", projectColumns ],
|
||||
[ "project_component", projectComponentColumns ],
|
||||
[ "movement", movementColumns ],
|
||||
];
|
||||
for (const [ tableName, columns ] of tables) {
|
||||
const path = `/${tableName.replace(/_(.)/g, (_, v) => v.toUpperCase())}`;
|
||||
api.get(`${path}`, c => {
|
||||
const { limit = "50", offset = "0" } = c.req.query();
|
||||
const parsedLimit = Math.max(0, Number(limit) || 50);
|
||||
const parsedOffset = Math.max(0, Number(offset) || 50);
|
||||
const stmt = db.prepare(`SELECT * FROM ${tableName} LIMIT ? OFFSET ?`);
|
||||
const countStmt = db.prepare(`SELECT COUNT(*) AS count FROM ${tableName}`);
|
||||
return c.json({
|
||||
data: stmt.all(parsedLimit, parsedOffset),
|
||||
limit: parsedLimit,
|
||||
offset: parsedOffset,
|
||||
count: countStmt.get()!.count,
|
||||
});
|
||||
});
|
||||
api.post(`${path}`, async c => {
|
||||
const data = await c.req.json();
|
||||
const { lastInsertRowid } = insertHelper(tableName, columns, data);
|
||||
return c.json({ id: lastInsertRowid });
|
||||
});
|
||||
api.get(`${path}/:id`, c => {
|
||||
const id = c.req.param("id");
|
||||
const stmt = db.prepare(`SELECT * FROM ${tableName} WHERE id = ?`);
|
||||
return c.json(stmt.get(id));
|
||||
});
|
||||
api.patch(`${path}/:id`, async c => {
|
||||
const id = parseInt(c.req.param("id"));
|
||||
const data = await c.req.json();
|
||||
patchHelper(tableName, columns, id, data);
|
||||
return c.json({});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
sql.run`CREATE TABLE IF NOT EXISTS version ( version INTEGER NOT NULL ) STRICT`;
|
||||
|
||||
let version = sql.get`SELECT version FROM version LIMIT 1`?.version;
|
||||
|
||||
console.log(`Current database version: ${version}`);
|
||||
|
||||
if (version === undefined) {
|
||||
wrapWithTransaction(() => {
|
||||
sql.run`CREATE TABLE location (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
cabinet TEXT,
|
||||
drawer TEXT,
|
||||
bin TEXT
|
||||
) STRICT`;
|
||||
|
||||
sql.run`CREATE TABLE component (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
partNumber TEXT NOT NULL,
|
||||
manufacturer TEXT,
|
||||
description TEXT,
|
||||
packaging TEXT,
|
||||
category TEXT,
|
||||
package TEXT,
|
||||
value TEXT,
|
||||
quantity INTEGER NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
location INTEGER,
|
||||
datasheet TEXT,
|
||||
FOREIGN KEY (location) REFERENCES location(id)
|
||||
) STRICT`;
|
||||
sql.run`CREATE INDEX component_location_idx ON component(location)`;
|
||||
|
||||
sql.run`CREATE TABLE project (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL
|
||||
) STRICT`;
|
||||
|
||||
sql.run`CREATE TABLE project_component (
|
||||
project INTEGER NOT NULL,
|
||||
component INTEGER NOT NULL,
|
||||
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
||||
PRIMARY KEY (project, component),
|
||||
FOREIGN KEY (component) REFERENCES component(id),
|
||||
FOREIGN KEY (project) REFERENCES project(id)
|
||||
) STRICT`;
|
||||
sql.run`CREATE INDEX project_component_component_idx ON project_component(component)`;
|
||||
|
||||
sql.run`CREATE TABLE movement (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
component INTEGER NOT NULL,
|
||||
quantity INTEGER NOT NULL,
|
||||
date INTEGER,
|
||||
reason TEXT,
|
||||
project INTEGER,
|
||||
price INTEGER,
|
||||
FOREIGN KEY (component) REFERENCES component(id),
|
||||
FOREIGN KEY (project) REFERENCES project(id)
|
||||
) STRICT`;
|
||||
sql.run`CREATE INDEX movement_component_idx ON movement(component)`;
|
||||
sql.run`CREATE INDEX movement_project_idx ON movement(project)`;
|
||||
|
||||
// language=SQLite - no idea why it's not doing that automatically here
|
||||
sql.run`INSERT INTO version (version) VALUES (1)`;
|
||||
console.log("initialized database to version 1");
|
||||
version = 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
app.route("/", api);
|
||||
showRoutes(app);
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port: Number(process.env.PORT ?? 3000),
|
||||
}, ({ port }) => {
|
||||
console.log(`Listening on port ${port}`);
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
root: "web",
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:3000",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "../dist",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Stock-keeping</title>
|
||||
<link rel="stylesheet" href="style.scss">
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
Components:
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>P/N</th>
|
||||
<th>description</th>
|
||||
<th>packaging</th>
|
||||
<th>category</th>
|
||||
<th>footprint</th>
|
||||
<th>value</th>
|
||||
<th>quantity</th>
|
||||
<th>unit</th>
|
||||
<th>location</th>
|
||||
<th>datasheet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template data-table-template="component">
|
||||
<tr>
|
||||
<td data-field="id" data-type="id">-</td>
|
||||
<td data-field="partNumber">-</td>
|
||||
<td data-field="description">-</td>
|
||||
<td data-field="packaging">-</td>
|
||||
<td data-field="category" data-type="category">-</td>
|
||||
<td data-field="package">-</td>
|
||||
<td data-field="value">-</td>
|
||||
<td data-field="quantity">-</td>
|
||||
<td data-field="unit">-</td>
|
||||
<td data-field="location" data-type="location">-</td>
|
||||
<td data-field="datasheet" data-type="url">-</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal">
|
||||
|
||||
</div>
|
||||
<script src="main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
window.addEventListener("load", () => {
|
||||
console.log("im here!");
|
||||
fetch("/api/component").then(e => e.json()).then(console.log);
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
:root {
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
Loading…
Reference in New Issue