ThreadCount Community edition

Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from 440e645 on 2026-09-13. Licensed under the Functional Source License (FSL-1.1-ALv2).
This commit is contained in:
ThreadCount
2026-09-13 12:12:58 +10:00
commit cbf533d90b
275 changed files with 43340 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
"use client";
// Device camera capture for attachments: opens the camera (file input with capture), downscales on a canvas
// to <= 900px and returns a JPEG data URL. Resolves null if the user cancels.
export function takePhoto(): Promise<string | null> {
return new Promise((resolve) => {
const inp = document.createElement("input");
inp.type = "file"; inp.accept = "image/*"; inp.setAttribute("capture", "environment");
let done = false;
const finish = (v: string | null) => { if (!done) { done = true; resolve(v); } };
inp.onchange = () => {
const f = inp.files && inp.files[0]; if (!f) { finish(null); return; }
const img = new Image();
img.onload = () => {
const max = 900; const sc = Math.min(1, max / Math.max(img.width, img.height));
const cv = document.createElement("canvas"); cv.width = Math.round(img.width * sc); cv.height = Math.round(img.height * sc);
cv.getContext("2d")!.drawImage(img, 0, 0, cv.width, cv.height);
URL.revokeObjectURL(img.src);
finish(cv.toDataURL("image/jpeg", 0.72));
};
img.onerror = () => finish(null);
img.src = URL.createObjectURL(f);
};
// Cancelling the picker fires no change event; treat focus returning without a file as a cancel.
window.addEventListener("focus", () => setTimeout(() => { if (!inp.files || !inp.files.length) finish(null); }, 800), { once: true });
inp.click();
});
}
/** Upload a data URL to the facility's photo store; returns the photo id. */
export async function uploadPhoto(mutate: (op: string, payload?: unknown) => Promise<{ ok: true; result: unknown } | { ok: false; error: string }>, kind: string, data: string): Promise<{ id: string } | { error: string }> {
const r = await mutate("photo.put", { kind, data });
if (!r.ok) return { error: r.error };
return { id: (r.result as { id: string }).id };
}
export const photoUrl = (id: string) => `/api/photo/${id}`;
export function viewPhoto(id: string) { window.open(photoUrl(id), "_blank", "noopener"); }