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