production: reminder cron, dashboard overhaul, shadcn components, setup wizard
- /api/cron/reminders: processes pending reminders every 15min, sends WhatsApp with email fallback - /api/cron/overdue: marks overdue pledges daily (7d deferred, 14d immediate) - /api/pledges: GET handler with filtering, search, pagination, sort by dueDate - Dashboard overview: stats, collection progress bar, needs attention, upcoming payments - Dashboard pledges: proper table with status tabs, search, actions, pagination - New shadcn components: Table, Tabs, DropdownMenu, Progress - Setup wizard: 4-step onboarding (org → bank → event → QR code) - Settings API: PUT handler for org create/update - Org resolver: single-tenant fallback to first org - Cron jobs installed: reminders every 15min, overdue check at 6am - Auto-generates installment dates when not provided - HOSTNAME=0.0.0.0 in compose for multi-network binding
This commit is contained in:
12
calvana-build/package.json
Normal file
12
calvana-build/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "calvana",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node server/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.0",
|
||||
"pg": "^8.13.0"
|
||||
}
|
||||
}
|
||||
153
calvana-build/server/index.js
Normal file
153
calvana-build/server/index.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const express = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'dokploy-postgres',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
user: process.env.DB_USER || 'dokploy',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'calvana',
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', async (req, res) => {
|
||||
try {
|
||||
await pool.query('SELECT 1');
|
||||
res.json({ status: 'ok', db: 'connected' });
|
||||
} catch (e) {
|
||||
res.status(500).json({ status: 'error', db: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET ships
|
||||
app.get('/api/ships', async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT * FROM ships ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST ship
|
||||
app.post('/api/ships', async (req, res) => {
|
||||
const { title, status, metric, details } = req.body;
|
||||
if (!title) return res.status(400).json({ error: 'title required' });
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'INSERT INTO ships (title, status, metric, details) VALUES ($1, $2, $3, $4) RETURNING *',
|
||||
[title, status || 'planned', metric || null, details || null]
|
||||
);
|
||||
res.status(201).json(rows[0]);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH ship
|
||||
app.patch('/api/ships/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { title, status, metric, details } = req.body;
|
||||
try {
|
||||
const sets = [];
|
||||
const vals = [];
|
||||
let i = 1;
|
||||
if (title !== undefined) { sets.push(`title=$${i++}`); vals.push(title); }
|
||||
if (status !== undefined) { sets.push(`status=$${i++}`); vals.push(status); }
|
||||
if (metric !== undefined) { sets.push(`metric=$${i++}`); vals.push(metric); }
|
||||
if (details !== undefined) { sets.push(`details=$${i++}`); vals.push(details); }
|
||||
if (sets.length === 0) return res.status(400).json({ error: 'nothing to update' });
|
||||
sets.push(`updated_at=NOW()`);
|
||||
vals.push(id);
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE ships SET ${sets.join(', ')} WHERE id=$${i} RETURNING *`,
|
||||
vals
|
||||
);
|
||||
if (rows.length === 0) return res.status(404).json({ error: 'not found' });
|
||||
res.json(rows[0]);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE ship
|
||||
app.delete('/api/ships/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'DELETE FROM ships WHERE id=$1 RETURNING *',
|
||||
[id]
|
||||
);
|
||||
if (rows.length === 0) return res.status(404).json({ error: 'not found' });
|
||||
res.json({ deleted: true, ship: rows[0] });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE oops
|
||||
app.delete('/api/oops/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'DELETE FROM oops WHERE id=$1 RETURNING *',
|
||||
[id]
|
||||
);
|
||||
if (rows.length === 0) return res.status(404).json({ error: 'not found' });
|
||||
res.json({ deleted: true, oops: rows[0] });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET oops
|
||||
app.get('/api/oops', async (req, res) => {
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT * FROM oops ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST oops
|
||||
app.post('/api/oops', async (req, res) => {
|
||||
const { description, fix_time, commit_link } = req.body;
|
||||
if (!description) return res.status(400).json({ error: 'description required' });
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
'INSERT INTO oops (description, fix_time, commit_link) VALUES ($1, $2, $3) RETURNING *',
|
||||
[description, fix_time || null, commit_link || null]
|
||||
);
|
||||
res.status(201).json(rows[0]);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static(path.join(__dirname, '..', 'html')));
|
||||
|
||||
// SPA fallback — serve index.html for unmatched routes
|
||||
app.get('*', (req, res) => {
|
||||
// Check if requesting a known page directory
|
||||
const pagePath = path.join(__dirname, '..', 'html', req.path, 'index.html');
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(pagePath)) {
|
||||
return res.sendFile(pagePath);
|
||||
}
|
||||
res.sendFile(path.join(__dirname, '..', 'html', 'index.html'));
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 80;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Calvana server listening on :${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user