diff --git a/client/src/pages/Settings.jsx b/client/src/pages/Settings.jsx index 9c07995..8a6f033 100644 --- a/client/src/pages/Settings.jsx +++ b/client/src/pages/Settings.jsx @@ -1,5 +1,238 @@ +import { useState, useEffect } from 'react'; + +const fieldStyle = { + display: 'flex', + flexDirection: 'column', + gap: '4px', + marginBottom: '16px', +}; + +const labelStyle = { + fontWeight: '600', + fontSize: '14px', +}; + +const inputStyle = { + padding: '8px 10px', + fontSize: '14px', + border: '1px solid #ccc', + borderRadius: '4px', + width: '180px', +}; + +const sectionStyle = { + marginBottom: '32px', +}; + +const sectionTitleStyle = { + fontSize: '18px', + fontWeight: '700', + marginBottom: '16px', + borderBottom: '2px solid #e5e7eb', + paddingBottom: '8px', +}; + +const submitStyle = { + padding: '10px 24px', + fontSize: '15px', + fontWeight: '600', + background: '#2563eb', + color: '#fff', + border: 'none', + borderRadius: '6px', + cursor: 'pointer', +}; + +const successStyle = { + color: '#16a34a', + fontWeight: '600', + marginTop: '12px', +}; + +const DEFAULT_FORM = { + paycheck1_day: '', + paycheck2_day: '', + paycheck1_gross: '', + paycheck1_net: '', + paycheck2_gross: '', + paycheck2_net: '', +}; + function Settings() { - return

Settings

Placeholder — coming soon.

; + const [form, setForm] = useState(DEFAULT_FORM); + const [saved, setSaved] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + fetch('/api/config') + .then((res) => res.json()) + .then((data) => { + setForm({ + paycheck1_day: data.paycheck1_day ?? '', + paycheck2_day: data.paycheck2_day ?? '', + paycheck1_gross: data.paycheck1_gross ?? '', + paycheck1_net: data.paycheck1_net ?? '', + paycheck2_gross: data.paycheck2_gross ?? '', + paycheck2_net: data.paycheck2_net ?? '', + }); + }) + .catch(() => setError('Failed to load settings.')); + }, []); + + function handleChange(e) { + const { name, value } = e.target; + setSaved(false); + setForm((prev) => ({ ...prev, [name]: value })); + } + + function handleSubmit(e) { + e.preventDefault(); + setError(null); + setSaved(false); + + const payload = {}; + for (const [key, val] of Object.entries(form)) { + if (val !== '' && val !== null) { + payload[key] = Number(val); + } + } + + fetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + .then((res) => { + if (!res.ok) throw new Error('Save failed'); + return res.json(); + }) + .then((data) => { + setForm({ + paycheck1_day: data.paycheck1_day ?? '', + paycheck2_day: data.paycheck2_day ?? '', + paycheck1_gross: data.paycheck1_gross ?? '', + paycheck1_net: data.paycheck1_net ?? '', + paycheck2_gross: data.paycheck2_gross ?? '', + paycheck2_net: data.paycheck2_net ?? '', + }); + setSaved(true); + }) + .catch(() => setError('Failed to save settings.')); + } + + return ( +
+

Settings

+ + {error &&

{error}

} + +
+
+
Pay Schedule
+ +
+ + +
+ +
+ + +
+
+ +
+
Paycheck Amounts
+ +
+
Paycheck 1
+
+ + +
+
+ + +
+
+ +
+
Paycheck 2
+
+ + +
+
+ + +
+
+
+ + + + {saved &&

Settings saved

} +
+
+ ); } export default Settings; diff --git a/server/src/index.js b/server/src/index.js index ce355ce..cdfd02e 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -3,6 +3,7 @@ const express = require('express'); const cors = require('cors'); const path = require('path'); const healthRouter = require('./routes/health'); +const configRouter = require('./routes/config'); const db = require('./db'); const app = express(); @@ -13,6 +14,7 @@ app.use(express.json()); // API routes app.use('/api', healthRouter); +app.use('/api', configRouter); // Serve static client files in production const clientDist = path.join(__dirname, '../../client/dist'); diff --git a/server/src/routes/config.js b/server/src/routes/config.js new file mode 100644 index 0000000..c1428d2 --- /dev/null +++ b/server/src/routes/config.js @@ -0,0 +1,82 @@ +const express = require('express'); +const router = express.Router(); +const { pool } = require('../db'); + +const CONFIG_KEYS = [ + 'paycheck1_day', + 'paycheck2_day', + 'paycheck1_gross', + 'paycheck1_net', + 'paycheck2_gross', + 'paycheck2_net', +]; + +const DEFAULTS = { + paycheck1_day: 1, + paycheck2_day: 15, +}; + +async function getAllConfig() { + const result = await pool.query( + 'SELECT key, value FROM config WHERE key = ANY($1)', + [CONFIG_KEYS] + ); + + const map = {}; + for (const row of result.rows) { + map[row.key] = row.value; + } + + const config = {}; + for (const key of CONFIG_KEYS) { + const raw = map[key] !== undefined ? map[key] : (DEFAULTS[key] !== undefined ? String(DEFAULTS[key]) : null); + config[key] = raw !== null ? Number(raw) : null; + } + + return config; +} + +router.get('/config', async (req, res) => { + try { + const config = await getAllConfig(); + res.json(config); + } catch (err) { + console.error('GET /api/config error:', err); + res.status(500).json({ error: 'Failed to fetch config' }); + } +}); + +router.put('/config', async (req, res) => { + try { + const updates = req.body; + const validKeys = Object.keys(updates).filter((k) => CONFIG_KEYS.includes(k)); + + if (validKeys.length > 0) { + const client = await pool.connect(); + try { + await client.query('BEGIN'); + for (const key of validKeys) { + await client.query( + `INSERT INTO config (key, value) VALUES ($1, $2) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`, + [key, String(updates[key])] + ); + } + await client.query('COMMIT'); + } catch (err) { + await client.query('ROLLBACK'); + throw err; + } finally { + client.release(); + } + } + + const config = await getAllConfig(); + res.json(config); + } catch (err) { + console.error('PUT /api/config error:', err); + res.status(500).json({ error: 'Failed to update config' }); + } +}); + +module.exports = router;