Add photo upload for PDQ Z-reports and receipt evidence
DB:
- Add attachment_type ('pdq_z_report' | 'receipt_error' | 'other') and
label columns to cash_count_attachments; migrate existing rows via
ALTER TABLE IF NOT EXISTS (no data loss)
Backend:
- Upload route now reads attachment_type and label from multipart form
fields alongside the file, stores them in the DB
Frontend:
- PhotoUploader component: thumbnail grid, upload button (images + PDF),
spinner during upload, ✕ delete button on each photo, click opens in
new tab
- DailyCashUp: PDQ Z-Reports card (one section per machine — Front Desk /
Restaurant Bar) appears after Card Machines when a cash up exists
- DailyCashUp: Receipt Evidence card at bottom for any discrepancy photos;
stays editable even on final cash ups so receipts can be added later
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1f5f1639c7
commit
43c332be9b
5 changed files with 166 additions and 21 deletions
|
|
@ -113,15 +113,19 @@ export async function initDb() {
|
|||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cash_count_attachments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
cash_up_id INTEGER NOT NULL REFERENCES cash_ups(id) ON DELETE CASCADE,
|
||||
file_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size BIGINT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
uploaded_by TEXT NOT NULL,
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
id SERIAL PRIMARY KEY,
|
||||
cash_up_id INTEGER NOT NULL REFERENCES cash_ups(id) ON DELETE CASCADE,
|
||||
file_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size BIGINT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
uploaded_by TEXT NOT NULL,
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
attachment_type TEXT NOT NULL DEFAULT 'other',
|
||||
label TEXT
|
||||
);
|
||||
ALTER TABLE cash_count_attachments ADD COLUMN IF NOT EXISTS attachment_type TEXT NOT NULL DEFAULT 'other';
|
||||
ALTER TABLE cash_count_attachments ADD COLUMN IF NOT EXISTS label TEXT;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
|
|
|
|||
|
|
@ -50,29 +50,40 @@ app.post('/api/attachments/upload/:cash_up_id', { preHandler: requireAuth }, asy
|
|||
const { rows } = await pool.query('SELECT id FROM cash_ups WHERE id = $1', [cashUpId])
|
||||
if (!rows.length) return reply.status(404).send({ error: 'Cash up not found' })
|
||||
|
||||
const data = await req.file()
|
||||
if (!data) return reply.status(400).send({ error: 'No file uploaded' })
|
||||
// Iterate multipart parts to collect file + text fields
|
||||
let fileData = null, attachmentType = 'other', label = null
|
||||
for await (const part of req.parts()) {
|
||||
if (part.type === 'file') {
|
||||
fileData = part
|
||||
} else {
|
||||
const val = await part.value
|
||||
if (part.fieldname === 'attachment_type') attachmentType = String(val || 'other')
|
||||
if (part.fieldname === 'label') label = String(val || '') || null
|
||||
}
|
||||
}
|
||||
if (!fileData) return reply.status(400).send({ error: 'No file uploaded' })
|
||||
|
||||
const allowed = ['image/jpeg', 'image/jpg', 'image/png', 'application/pdf']
|
||||
if (!allowed.includes(data.mimetype)) {
|
||||
if (!allowed.includes(fileData.mimetype)) {
|
||||
return reply.status(400).send({ error: 'Only JPEG, PNG and PDF files are allowed' })
|
||||
}
|
||||
|
||||
const ext = extname(data.filename) || '.bin'
|
||||
const ext = extname(fileData.filename) || '.bin'
|
||||
const filename = randomUUID() + ext
|
||||
const dir = join(UPLOADS_DIR, 'cashup', String(cashUpId))
|
||||
await mkdir(dir, { recursive: true })
|
||||
|
||||
let size = 0
|
||||
const dest = createWriteStream(join(dir, filename))
|
||||
for await (const chunk of data.file) { dest.write(chunk); size += chunk.length }
|
||||
for await (const chunk of fileData.file) { dest.write(chunk); size += chunk.length }
|
||||
await new Promise(r => dest.end(r))
|
||||
|
||||
const filePath = `/cashup/${cashUpId}/${filename}`
|
||||
const { rows: ins } = await pool.query(
|
||||
`INSERT INTO cash_count_attachments (cash_up_id, file_name, file_path, file_size, mime_type, uploaded_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`,
|
||||
[cashUpId, data.filename, filePath, size, data.mimetype, req.user.email]
|
||||
`INSERT INTO cash_count_attachments
|
||||
(cash_up_id, file_name, file_path, file_size, mime_type, uploaded_by, attachment_type, label)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
|
||||
[cashUpId, fileData.filename, filePath, size, fileData.mimetype, req.user.email, attachmentType, label]
|
||||
)
|
||||
|
||||
return ins.rows[0]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue