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}
}
+
+
+
+ );
}
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;