Files
Budget_Tool_Martinhal/budget-app/frontend/src/pages/Settings.jsx
T

115 lines
5.7 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()} />
}