Recurring Visit Button

This commit is contained in:
jessikitty committed 2026-09-30 23:01:32 +10:00
1 parent 215c38849b
commit 1b1d950d71
4 files changed
+156 -1

No files matched your search

+92 -1
View File
@@ -5,7 +5,13 @@ import fs from 'node:fs';
import db from '../db.js';
import config from '../config.js';
import { decryptPin, encryptPin, generatePin, pinLookup } from '../pins.js';
import { photoAbsolutePath, deletePhoto, purgeOldPhotos, savePhoto } from '../photos.js';
import {
photoAbsolutePath,
deletePhoto,
purgeOldPhotos,
savePhoto,
copyStoredPhoto,
} from '../photos.js';
import * as sheets from '../sheets.js';
import * as tls from '../tls.js';
import * as printer from '../printer.js';
@@ -1116,6 +1122,91 @@ router.get('/visits', (req, res) => {
res.json(db.prepare(sql).all(...params).map(shapeVisit));
});
/**
* Promotes someone who is signing in as a guest into a saved recurring visitor,
* using the details they already gave at the kiosk. A random PIN is issued, and
* their photo is carried across so the kiosk stops asking them to pose.
*
* The visit itself is left alone: it happened as a guest visit and the log should
* go on saying so.
*/
router.post('/visits/:id/make-regular', (req, res) => {
const visit = db.prepare('SELECT * FROM visits WHERE id = ?').get(req.params.id);
if (!visit) return res.status(404).json({ error: 'Not found.' });
try {
if (scopedSiteId(req)) assertSiteAllowed(req, visit.site_id);
} catch (err) {
return res.status(403).json({ error: err.message });
}
if (!visit.phone) {
return res.status(400).json({
error: 'A mobile number is needed, because it becomes their sign in name. This visitor only left an email address.',
});
}
const phone = normalisePhone(visit.phone);
const email = normaliseEmail(visit.email);
const byPhone = db
.prepare('SELECT id, first_name, last_name FROM frequent_visitors WHERE phone = ?')
.get(phone);
if (byPhone) {
return res.status(409).json({
error: `${byPhone.first_name} ${byPhone.last_name} is already saved with that mobile number.`,
existingId: byPhone.id,
});
}
if (email) {
const byEmail = db
.prepare('SELECT id, first_name, last_name FROM frequent_visitors WHERE lower(email) = ?')
.get(email);
if (byEmail) {
return res.status(409).json({
error: `${byEmail.first_name} ${byEmail.last_name} is already saved with that email address.`,
existingId: byEmail.id,
});
}
}
try {
const pin = allocatePin(null);
// Their sign-in photo becomes the one kept on file. Copied rather than shared,
// so retention purging the visit cannot remove their profile photo.
const photoPath = visit.photo_path ? copyStoredPhoto(visit.photo_path) : null;
const info = db
.prepare(
`INSERT INTO frequent_visitors
(first_name, last_name, company, phone, email, check_type, check_number, check_expiry,
default_host_id, site_id, pin_enc, pin_lookup, photo_path, notes, active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`
)
.run(
visit.first_name,
visit.last_name,
visit.company || null,
phone,
email || null,
visit.check_type || 'NONE',
visit.check_number || null,
visit.check_expiry || null,
visit.host_id || null,
visit.site_id || null,
encryptPin(pin),
pinLookup(pin),
photoPath,
null
);
const created = db.prepare('SELECT * FROM frequent_visitors WHERE id = ?').get(info.lastInsertRowid);
res.json({ ...shapeFrequent(created, true), photoCarriedOver: Boolean(photoPath) });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
router.post('/visits/:id/signout', (req, res) => {
const visit = db
.prepare('SELECT * FROM visits WHERE id = ? AND signed_out_at IS NULL')