Unzip budget-app v7 into repo root

This commit is contained in:
jpmvaz
2026-10-29 20:40:02 +00:00
parent 2554e68315
commit c9fd26f1c7
94 changed files with 12098 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
import { useCallback, useEffect, useState } from 'react'
import { api } from '../api.js'
import { atLeast, useAuth } from '../auth.js'
import { useT } from '../i18n.js'
import { ErrorNote } from '../components/ui.jsx'
export default function Settings() {
const t = useT()
const { user } = useAuth()
const admin = atLeast(user, 'admin')
const [cats, setCats] = useState([])
const [sites, setSites] = useState([])
const [depts, setDepts] = useState([])
const [deptId, setDeptId] = useState(user.department_id || '')
const [copySource, setCopySource] = useState('defaults')
const [notice, setNotice] = useState(null)
const [error, setError] = useState(null)
const [newCat, setNewCat] = useState('')
const [newSite, setNewSite] = useState('')
useEffect(() => {
api.get('/api/departments').then((ds) => {
setDepts(ds)
setDeptId((cur) => cur || ds[0]?.id || '')
})
}, [])
const load = useCallback(() => {
if (deptId) api.get(`/api/categories?department_id=${deptId}`).then(setCats)
api.get('/api/sites').then(setSites)
}, [deptId])
useEffect(() => { load() }, [load])
const editableDepts = admin ? depts : depts.filter((d) => d.id === user.department_id)
const run = async (fn) => {
try {
await fn()
setError(null)
} catch (e) { setError(e) }
load()
}
const putCat = (c, patch) => run(() => api.put(`/api/categories/${c.id}`, patch))
const putSite = (s, patch) => run(() => api.put(`/api/sites/${s.id}`, patch))
return (
<div>
<h1>{admin ? t('nav_settings') : t('nav_categories')}</h1>
<ErrorNote error={error} />
{notice && <div className="notice">{notice}</div>}
{admin && <section className="card">
<h2>{t('sites')}</h2>
<table className="plain">
<tbody>
{sites.map((s) => (
<tr key={s.id}>
<td><Inline value={s.name} onSave={(v) => putSite(s, { name: v })} /></td>
<td><label className="check"><input type="checkbox" checked={s.active} onChange={(e) => putSite(s, { active: e.target.checked })} /> {t('active')}</label></td>
</tr>
))}
</tbody>
</table>
<form className="inline-add" onSubmit={(e) => { e.preventDefault(); newSite.trim() && run(() => api.post('/api/sites', { name: newSite })).then(() => setNewSite('')) }}>
<input placeholder={t('new_site')} value={newSite} onChange={(e) => setNewSite(e.target.value)} />
<button>{t('create')}</button>
</form>
</section>}
<section className="card">
<div className="cat-head flush">
<h2>{t('categories')}</h2>
<select value={deptId} onChange={(e) => setDeptId(Number(e.target.value))} aria-label={t('department')} className="auto">
{editableDepts.map((d) => <option key={d.id} value={d.id}>{d.name}</option>)}
</select>
<select value={copySource} onChange={(e) => setCopySource(e.target.value)} aria-label={t('copy_categories')} className="auto">
<option value="defaults">{t('categories_defaults')}</option>
{depts.filter((d) => d.id !== deptId).map((d) => <option key={d.id} value={d.id}>{t('categories_copy', { name: d.name })}</option>)}
</select>
<button onClick={() => run(() => api.post(`/api/departments/${deptId}/categories/copy`, { source: copySource })).then(() => setNotice(t('copy_categories_done')))}>{t('copy_categories')}</button>
</div>
<div className="table-wrap">
<table className="plain">
<thead><tr><th>{t('order')}</th><th>{t('name_en')}</th><th>{t('name_pt')}</th><th>{t('section')}</th><th>{t('active')}</th><th /></tr></thead>
<tbody>
{cats.map((c) => (
<tr key={c.id} className={c.active ? '' : 'muted'}>
<td><Inline value={String(c.sort_order)} short onSave={(v) => putCat(c, { sort_order: Number(v) || 0 })} /></td>
<td><Inline value={c.name_en} onSave={(v) => putCat(c, { name_en: v })} /></td>
<td><Inline value={c.name_pt} onSave={(v) => putCat(c, { name_pt: v })} /></td>
<td>
<select className="cell" value={c.section} onChange={(e) => putCat(c, { section: e.target.value })}>
<option value="cost">{t('section_cost')}</option><option value="revenue">{t('section_revenue')}</option>
</select>
</td>
<td><input type="checkbox" checked={c.active} onChange={(e) => putCat(c, { active: e.target.checked })} aria-label={t('active')} /></td>
<td className="right"><button className="icon danger" aria-label={t('delete')} title={t('delete')}
onClick={() => window.confirm(t('confirm_delete', { name: c.name_en })) && run(() => api.del(`/api/categories/${c.id}`))}>×</button></td>
</tr>
))}
</tbody>
</table>
</div>
<form className="inline-add" onSubmit={(e) => { e.preventDefault(); newCat.trim() && run(() => api.post('/api/categories', { name_en: newCat, department_id: deptId })).then(() => setNewCat('')) }}>
<input placeholder={t('new_category')} value={newCat} onChange={(e) => setNewCat(e.target.value)} />
<button>{t('create')}</button>
</form>
</section>
</div>
)
}
function Inline({ value, onSave, short }) {
const [text, setText] = useState(value)
useEffect(() => setText(value), [value])
return <input className={'cell' + (short ? ' short' : '')} value={text} onChange={(e) => setText(e.target.value)}
onBlur={() => text !== value && onSave(text)} onKeyDown={(e) => e.key === 'Enter' && e.target.blur()} />
}