feat: založení základní stránky příjem/výdej + import a statistiky
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Form, Modal } from "react-bootstrap";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { UserSettings, getSettings, saveBasalCalories, saveSourceRate } from "../../../../types";
|
||||
import { formatPriceInput, parseIntInput, parsePriceInput } from "../../Utils";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean,
|
||||
onClose: () => void,
|
||||
};
|
||||
|
||||
/**
|
||||
* Nastavení sazeb za 100 g u podniků, které účtují podle váhy.
|
||||
*
|
||||
* Sazba slouží jako výchozí hodnota při zadávání jídla — u konkrétní položky
|
||||
* ji jde přepsat, protože salát bývá účtovaný jinak než hlavní jídlo.
|
||||
*/
|
||||
export default function SettingsModal({ isOpen, onClose }: Readonly<Props>) {
|
||||
const [settings, setSettings] = useState<UserSettings | undefined>();
|
||||
const [newSource, setNewSource] = useState('');
|
||||
const [newRate, setNewRate] = useState('');
|
||||
const [basal, setBasal] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setNewSource('');
|
||||
setNewRate('');
|
||||
getSettings().then(response => {
|
||||
setSettings(response.data);
|
||||
setBasal(response.data?.basalCalories != null ? String(response.data.basalCalories) : '');
|
||||
});
|
||||
}, [isOpen]);
|
||||
|
||||
const save = async (source: string, pricePer100g: number | null) => {
|
||||
setSaving(true);
|
||||
const response = await saveSourceRate({ body: { source, pricePer100g } });
|
||||
setSaving(false);
|
||||
if (response.data) {
|
||||
setSettings(response.data);
|
||||
}
|
||||
};
|
||||
|
||||
/** Uloží klidový výdej. Prázdné pole ho odstraní. */
|
||||
const saveBasal = async () => {
|
||||
setSaving(true);
|
||||
const response = await saveBasalCalories({ body: { basalCalories: parseIntInput(basal) ?? null } });
|
||||
setSaving(false);
|
||||
if (response.data) {
|
||||
setSettings(response.data);
|
||||
}
|
||||
};
|
||||
|
||||
const addRate = async () => {
|
||||
const rate = parsePriceInput(newRate);
|
||||
if (!newSource.trim().length || !rate) return;
|
||||
await save(newSource.trim(), rate);
|
||||
setNewSource('');
|
||||
setNewRate('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onHide={onClose} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Nastavení</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<h3 className="settings-section-title">Klidový výdej</h3>
|
||||
<p className="text-body-secondary small">
|
||||
Kolik kcal spálíte za den bez pohybu (bazální metabolismus). Bez něj
|
||||
porovnává bilance dne jen jídlo proti pohybu a nejde o skutečný deficit.
|
||||
</p>
|
||||
<div className="rate-form mb-4">
|
||||
<div className="input-group">
|
||||
<Form.Control
|
||||
inputMode="numeric"
|
||||
value={basal}
|
||||
placeholder="např. 1600"
|
||||
onChange={event => setBasal(event.target.value.replace(/\D/g, ''))}
|
||||
/>
|
||||
<span className="input-group-text">kcal / den</span>
|
||||
</div>
|
||||
<Button onClick={saveBasal} disabled={saving}>Uložit</Button>
|
||||
</div>
|
||||
|
||||
<h3 className="settings-section-title">Cena za 100 g</h3>
|
||||
<p className="text-body-secondary small">
|
||||
U podniků, které účtují podle váhy, se z ceny jídla a této sazby dopočítá
|
||||
gramáž a z ní kalorie. U konkrétního jídla jde sazbu vždy přepsat.
|
||||
</p>
|
||||
|
||||
{settings?.sourceRates.length === 0 && (
|
||||
<p className="text-body-secondary small fst-italic">Zatím nemáte žádnou sazbu.</p>
|
||||
)}
|
||||
|
||||
{settings?.sourceRates.map(rate => (
|
||||
<div className="rate-row" key={rate.source}>
|
||||
<span className="rate-source">{rate.source}</span>
|
||||
<span className="rate-value">{formatPriceInput(rate.pricePer100g)} Kč / 100 g</span>
|
||||
<button
|
||||
className="btn-icon danger"
|
||||
onClick={() => save(rate.source, null)}
|
||||
disabled={saving}
|
||||
title="Odebrat sazbu"
|
||||
aria-label={`Odebrat sazbu ${rate.source}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Form
|
||||
className="rate-form"
|
||||
onSubmit={event => { event.preventDefault(); addRate(); }}
|
||||
>
|
||||
<Form.Control
|
||||
value={newSource}
|
||||
placeholder="Podnik, např. TechTower"
|
||||
onChange={event => setNewSource(event.target.value)}
|
||||
/>
|
||||
<div className="input-group rate-form-price">
|
||||
<Form.Control
|
||||
inputMode="decimal"
|
||||
value={newRate}
|
||||
placeholder="44,00"
|
||||
onChange={event => setNewRate(event.target.value)}
|
||||
/>
|
||||
<span className="input-group-text">Kč</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={addRate}
|
||||
disabled={saving || !newSource.trim().length || !parsePriceInput(newRate)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="outline-secondary" onClick={onClose}>Zavřít</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user