Unzip budget-app v7 into repo root
This commit is contained in:
@@ -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()} />
|
||||
}
|
||||
Reference in New Issue
Block a user