115 lines
5.7 KiB
React
115 lines
5.7 KiB
React
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()} />
|
|||
|
|
}
|