Add /categorize, /problems and /report
This commit is contained in:
parent
63f36a7ab0
commit
8082db19ac
9 changed files with 891 additions and 83 deletions
228
src/Categorize.jsx
Normal file
228
src/Categorize.jsx
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useState, useEffect} from "react";
|
||||||
|
import {useParams, useNavigate} from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
categorize
|
||||||
|
} from "./apiService";
|
||||||
|
|
||||||
|
const Categorize = (props) => {
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
const [report, setReport] = useState({});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
console.log("Categorize: calling useEffect");
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Categorize: useEffect");
|
||||||
|
console.log("params", params);
|
||||||
|
let redo = params.redo == 'true';
|
||||||
|
|
||||||
|
categorize(redo).then(
|
||||||
|
data => {
|
||||||
|
console.log("Categorize.useEffect categorize data", data);
|
||||||
|
if(data.status === 401) {
|
||||||
|
navigate('/login', {replace: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(data.status !== 200) {
|
||||||
|
setIsLoaded(true);
|
||||||
|
setError(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoaded(true);
|
||||||
|
setReport(data.result);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if(error.status === 401) {
|
||||||
|
navigate('/login', {replace: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('error object', error);
|
||||||
|
setIsLoaded(true);
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [navigate, params]);
|
||||||
|
|
||||||
|
if(error) {
|
||||||
|
return <div>Error: {error.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isLoaded) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFlagNames = function(flags) {
|
||||||
|
let names = [];
|
||||||
|
if(flags & 0x02) {
|
||||||
|
names.push("Case Insensitive");
|
||||||
|
}
|
||||||
|
if ((flags & 0x08) !== 0) {
|
||||||
|
names.push("Multiline");
|
||||||
|
}
|
||||||
|
if ((flags & 0x20) !== 0) {
|
||||||
|
names.push("Dotall");
|
||||||
|
}
|
||||||
|
if ((flags & 0x40) !== 0) {
|
||||||
|
names.push("Unicode Case");
|
||||||
|
}
|
||||||
|
if ((flags & 0x80) !== 0) {
|
||||||
|
names.push("Canon EQ");
|
||||||
|
}
|
||||||
|
if ((flags & 0x01) !== 0) {
|
||||||
|
names.push("Unix Linex");
|
||||||
|
}
|
||||||
|
if ((flags & 0x10) !== 0) {
|
||||||
|
names.push("Literal");
|
||||||
|
}
|
||||||
|
if ((flags & 0x100) !== 0) {
|
||||||
|
names.push("Unicode Character Class");
|
||||||
|
}
|
||||||
|
if ((flags & 0x04) !== 0) {
|
||||||
|
names.push("Comments");
|
||||||
|
}
|
||||||
|
return names.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberFormat = new Intl.NumberFormat("en-US", { style: 'currency', currency: 'USD' });
|
||||||
|
|
||||||
|
console.log("report render", report);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Categorize Result</h1>
|
||||||
|
{
|
||||||
|
Object.values(report).map(yearReport => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>{yearReport.year}</h2>
|
||||||
|
<h3>Multiply Assigned Transactions</h3>
|
||||||
|
<table className={"mat"}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>source</th>
|
||||||
|
<th>description</th>
|
||||||
|
<th>date</th>
|
||||||
|
<th>amount</th>
|
||||||
|
<th>regexes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
yearReport.multiplyAssignedTransactions.map(mat => {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{mat.transaction.transactionId}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{mat.transaction.source}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{mat.transaction.description}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{mat.transaction.date}
|
||||||
|
</td>
|
||||||
|
<td className={"amount"}>
|
||||||
|
{numberFormat.format(mat.transaction.amount)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Regular Expression</th>
|
||||||
|
<th>Flags</th>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Extra Description</th>
|
||||||
|
<th>Year</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
mat.regexes.map(regex => {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{regex.fqCategoryName}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{regex.source}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{regex.regex}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{getFlagNames(regex.flags)}
|
||||||
|
</td>
|
||||||
|
<td style={{textAlign: "right"}}>
|
||||||
|
{regex.priority}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{regex.description}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{regex.year}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<h3>Unassigned Transactions</h3>
|
||||||
|
<table className={"ut"}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Year</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{
|
||||||
|
yearReport.unassignedTransactions.map(transaction => {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{transaction.year}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{transaction.source}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{transaction.description}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{transaction.date}
|
||||||
|
</td>
|
||||||
|
<td className={"amount"}>
|
||||||
|
{numberFormat.format(transaction.amount)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Categorize;
|
||||||
|
|
@ -29,8 +29,8 @@ const EditRegex = (props) => {
|
||||||
const [addingCategory, setAddingCategory] = useState(null);
|
const [addingCategory, setAddingCategory] = useState(null);
|
||||||
const [addCategoryOkButtonDisabled, setAddCategoryOkButtonDisabled] = useState(true);
|
const [addCategoryOkButtonDisabled, setAddCategoryOkButtonDisabled] = useState(true);
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [attachedTransactions, setAttachedTransactions] = useState(null);
|
const [attachedTransactions, setAttachedTransactions] = useState([]);
|
||||||
const [allTransactions, setAllTransactions] = useState(null);
|
const [allTransactions, setAllTransactions] = useState([]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -115,9 +115,8 @@ const EditRegex = (props) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('start useEffect 2', regexObj);
|
console.log('start useEffect 2', regexObj);
|
||||||
let ignore = false;
|
let ignore = false;
|
||||||
|
let categoryId = regexObj != null ? regexObj.categoryId : null;
|
||||||
if(regexObj && regexObj.categoryId) {
|
getCategoryAncestry(categoryId).then(
|
||||||
getCategoryAncestry(regexObj.categoryId).then(
|
|
||||||
data => {
|
data => {
|
||||||
// console.log('useEffect 2 getCategoryAncestry data', data);
|
// console.log('useEffect 2 getCategoryAncestry data', data);
|
||||||
if(!ignore) {
|
if(!ignore) {
|
||||||
|
|
@ -127,6 +126,7 @@ const EditRegex = (props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
if(regexObj && regexObj.id) {
|
||||||
getTransactionsByRegexId("2025", regexObj.id).then(
|
getTransactionsByRegexId("2025", regexObj.id).then(
|
||||||
data => {
|
data => {
|
||||||
if(!ignore) {
|
if(!ignore) {
|
||||||
|
|
@ -134,6 +134,7 @@ const EditRegex = (props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
getAllTransactions("2025").then(
|
getAllTransactions("2025").then(
|
||||||
data => {
|
data => {
|
||||||
if(!ignore) {
|
if(!ignore) {
|
||||||
|
|
@ -141,7 +142,6 @@ const EditRegex = (props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
return () => {
|
return () => {
|
||||||
ignore = true;
|
ignore = true;
|
||||||
};
|
};
|
||||||
|
|
@ -327,7 +327,7 @@ const EditRegex = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCategoryChange = e => {
|
const handleCategoryChange = e => {
|
||||||
// console.log('EditRegex.handleCategoryChange', e);
|
console.log('EditRegex.handleCategoryChange', e);
|
||||||
const name = e.target.name;
|
const name = e.target.name;
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
let categoryLevel = +name;
|
let categoryLevel = +name;
|
||||||
|
|
@ -341,6 +341,10 @@ const EditRegex = (props) => {
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
console.error('error object', error);
|
console.error('error object', error);
|
||||||
});
|
});
|
||||||
|
let newRegexObj = getCopyOfRegexObj();
|
||||||
|
newRegexObj.categoryId = categoryId;
|
||||||
|
setRegexObj(newRegexObj);
|
||||||
|
setDirty(newRegexObj.categoryId !== originalRegexObj.categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryAncestryWasChanged = (categoryAncestry) => {
|
const categoryAncestryWasChanged = (categoryAncestry) => {
|
||||||
|
|
@ -507,7 +511,7 @@ const EditRegex = (props) => {
|
||||||
|
|
||||||
const categorySelects = [];
|
const categorySelects = [];
|
||||||
// console.log("render categoryAncestry", categoryAncestry);
|
// console.log("render categoryAncestry", categoryAncestry);
|
||||||
if(categoryAncestry) {
|
if(categoryAncestry.length > 0) {
|
||||||
for(let category of categoryAncestry) {
|
for(let category of categoryAncestry) {
|
||||||
// console.log("category", category);
|
// console.log("category", category);
|
||||||
let key = categorySelects.length;
|
let key = categorySelects.length;
|
||||||
|
|
@ -550,6 +554,20 @@ const EditRegex = (props) => {
|
||||||
categorySelects.push(select);
|
categorySelects.push(select);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
let key = categorySelects.length;
|
||||||
|
let options = [];
|
||||||
|
options.push(<option key={-2} value={"-2"}>?Select one</option>)
|
||||||
|
options.push(<option key={-1} value={"-1"}>+Create</option>);
|
||||||
|
// console.log("options", options);
|
||||||
|
let select = <select key={key} value={-2}
|
||||||
|
name={categorySelects.length}
|
||||||
|
onChange={handleCategoryChange}>
|
||||||
|
{options}
|
||||||
|
</select>;
|
||||||
|
// console.log("select", select);
|
||||||
|
categorySelects.push(select);
|
||||||
|
}
|
||||||
|
|
||||||
let attachedTransactionRows = [];
|
let attachedTransactionRows = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
@ -570,8 +588,15 @@ const EditRegex = (props) => {
|
||||||
if(regexObj.flags & 2) {
|
if(regexObj.flags & 2) {
|
||||||
flagsStr += "i";
|
flagsStr += "i";
|
||||||
}
|
}
|
||||||
let re = new RegExp(regexObj.regex, flagsStr);
|
let re = null;
|
||||||
|
try {
|
||||||
|
re = new RegExp(regexObj.regex, flagsStr);
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
re = null;
|
||||||
|
}
|
||||||
let matchedTransactionRows = [];
|
let matchedTransactionRows = [];
|
||||||
|
if(re) {
|
||||||
i = 0;
|
i = 0;
|
||||||
for(let transaction of allTransactions) {
|
for(let transaction of allTransactions) {
|
||||||
let regexMatches = re.test(transaction.description);
|
let regexMatches = re.test(transaction.description);
|
||||||
|
|
@ -592,6 +617,7 @@ const EditRegex = (props) => {
|
||||||
</tr>);
|
</tr>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("returning page");
|
console.log("returning page");
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,12 @@ const Regexes = (props) => {
|
||||||
deleteRegex(regex.id).then(
|
deleteRegex(regex.id).then(
|
||||||
data => {
|
data => {
|
||||||
console.log('deleteRegex data', data);
|
console.log('deleteRegex data', data);
|
||||||
|
var index = regexes.indexOf(regex);
|
||||||
|
if(index > -1) {
|
||||||
|
var newRegexes = regexes.slice();
|
||||||
|
newRegexes.splice(index, 1);
|
||||||
|
setRegexes(newRegexes);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
console.error('error object', error);
|
console.error('error object', error);
|
||||||
|
|
|
||||||
354
src/Report.jsx
Normal file
354
src/Report.jsx
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useState, useEffect} from "react";
|
||||||
|
import {useSearchParams, useNavigate, useLocation} from 'react-router-dom';
|
||||||
|
import {report} from "./apiService";
|
||||||
|
|
||||||
|
const Report = (props) => {
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
const [reportMap, setReportMap] = useState({});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const [years, setYears] = useState(null);
|
||||||
|
const [startDate, setStartDate] = useState(null);
|
||||||
|
const [endDate, setEndDate] = useState(null);
|
||||||
|
const [includes, setIncludes] = useState(null);
|
||||||
|
|
||||||
|
console.log("Report: calling useEffect");
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Report: useEffect");
|
||||||
|
console.log("searchParams", searchParams);
|
||||||
|
console.log('location', location);
|
||||||
|
let tmpYears = searchParams.get("years");
|
||||||
|
if(tmpYears == null && location.state != null) {
|
||||||
|
tmpYears = location.state.years;
|
||||||
|
}
|
||||||
|
setYears(tmpYears);
|
||||||
|
let tmpStartDate = searchParams.get("startDate");
|
||||||
|
if(tmpStartDate == null && location.state != null) {
|
||||||
|
tmpStartDate = location.state.startDate;
|
||||||
|
}
|
||||||
|
setStartDate(tmpStartDate);
|
||||||
|
let tmpEndDate = searchParams.get("endDate");
|
||||||
|
if(tmpEndDate == null && location.state != null) {
|
||||||
|
tmpEndDate = location.state.endDate;
|
||||||
|
}
|
||||||
|
setEndDate(tmpEndDate);
|
||||||
|
let tmpIncludes = searchParams.get("includes");
|
||||||
|
if(tmpIncludes == null && location.state != null) {
|
||||||
|
tmpIncludes = location.state.includes;
|
||||||
|
}
|
||||||
|
setIncludes(tmpIncludes);
|
||||||
|
window.localStorage.setItem('report-years', tmpYears);
|
||||||
|
window.localStorage.setItem('report-startDate', tmpStartDate);
|
||||||
|
window.localStorage.setItem('report-endDate', tmpEndDate);
|
||||||
|
window.localStorage.setItem('report-includes', tmpIncludes);
|
||||||
|
console.log(tmpYears, tmpStartDate, tmpEndDate, tmpIncludes);
|
||||||
|
report(tmpYears, tmpStartDate, tmpEndDate, tmpIncludes).then(
|
||||||
|
data => {
|
||||||
|
console.log("Report.useEffect report() data", data);
|
||||||
|
if(data.status === 401) {
|
||||||
|
navigate('/login', {replace: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(data.status !== 200) {
|
||||||
|
setIsLoaded(true);
|
||||||
|
setError(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoaded(true);
|
||||||
|
setReportMap(data.result);
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
if(error.status === 401) {
|
||||||
|
navigate('/login', {replace: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('error object', error);
|
||||||
|
setIsLoaded(true);
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [navigate, location, searchParams]);
|
||||||
|
|
||||||
|
if(error) {
|
||||||
|
return <div>Error: {error.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isLoaded) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFlagNames = function(flags) {
|
||||||
|
let names = [];
|
||||||
|
if(flags & 0x02) {
|
||||||
|
names.push("Case Insensitive");
|
||||||
|
}
|
||||||
|
if ((flags & 0x08) !== 0) {
|
||||||
|
names.push("Multiline");
|
||||||
|
}
|
||||||
|
if ((flags & 0x20) !== 0) {
|
||||||
|
names.push("Dotall");
|
||||||
|
}
|
||||||
|
if ((flags & 0x40) !== 0) {
|
||||||
|
names.push("Unicode Case");
|
||||||
|
}
|
||||||
|
if ((flags & 0x80) !== 0) {
|
||||||
|
names.push("Canon EQ");
|
||||||
|
}
|
||||||
|
if ((flags & 0x01) !== 0) {
|
||||||
|
names.push("Unix Linex");
|
||||||
|
}
|
||||||
|
if ((flags & 0x10) !== 0) {
|
||||||
|
names.push("Literal");
|
||||||
|
}
|
||||||
|
if ((flags & 0x100) !== 0) {
|
||||||
|
names.push("Unicode Character Class");
|
||||||
|
}
|
||||||
|
if ((flags & 0x04) !== 0) {
|
||||||
|
names.push("Comments");
|
||||||
|
}
|
||||||
|
return names.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderCategoryName = (category, indent) => {
|
||||||
|
if(category.details.length > 0) {
|
||||||
|
return (
|
||||||
|
<td style={{paddingLeft: "" + indent + "px", cursor: "pointer"}} onClick={handleCategoryNameClick}>
|
||||||
|
{ category.name }
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<td style={{paddingLeft: "" + indent + "px"}}>
|
||||||
|
{ category.name }
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const numberFormat = new Intl.NumberFormat("en-US", { style: 'currency', currency: 'USD' });
|
||||||
|
|
||||||
|
let renderMonths = (category) => {
|
||||||
|
let cols = [];
|
||||||
|
for(let monthNum = 0; monthNum < category.monthCount; monthNum++) {
|
||||||
|
let amount = category.monthGrandTotals[monthNum];
|
||||||
|
if(amount == null) {
|
||||||
|
amount = 0;
|
||||||
|
}
|
||||||
|
let style = {textAlign: "right", display: "none"};
|
||||||
|
if(category.largestMonth === monthNum) {
|
||||||
|
style["color"] = "RED";
|
||||||
|
}
|
||||||
|
cols.push(<td key={monthNum} style={style} className={"month" + monthNum}>{numberFormat.format(amount)}</td>);
|
||||||
|
}
|
||||||
|
return cols;
|
||||||
|
};
|
||||||
|
|
||||||
|
let renderDetail = (key, category) => {
|
||||||
|
let rows = [];
|
||||||
|
for(let detail of category.details) {
|
||||||
|
rows.push(<tr key={key + "-detail-" + rows.length}>
|
||||||
|
<td>{detail.transactionId}</td>
|
||||||
|
<td>{detail.source}</td>
|
||||||
|
<td>{detail.description}</td>
|
||||||
|
<td>{detail.date}</td>
|
||||||
|
<td style={{textAlign: "right"}}>{numberFormat.format(detail.amount)}</td>
|
||||||
|
<td>{detail.extraDescription}</td>
|
||||||
|
<td>{detail.regex}</td>
|
||||||
|
<td>{getFlagNames(detail.flags)}</td>
|
||||||
|
<td>{detail.requiredSource}</td>
|
||||||
|
</tr>)
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
let handleCategoryNameClick = (e) => {
|
||||||
|
// alert("clicked " + e.target);
|
||||||
|
let row = e.target.parentElement.nextSibling;
|
||||||
|
let d = row.style.display;
|
||||||
|
row.style.display = d === "none" ? "" : "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
let renderCategory = (level, year, category) => {
|
||||||
|
let indent = level * 10;
|
||||||
|
let result = [];
|
||||||
|
let key = "key-" + year + "-" + category.id;
|
||||||
|
result.push(
|
||||||
|
<>
|
||||||
|
<tr key={key + "-1"}>
|
||||||
|
{ renderCategoryName(category, indent) }
|
||||||
|
<td style={{textAlign: "right", display: "none"}} className={"average"}>
|
||||||
|
{ numberFormat.format(category.grandAverage) }
|
||||||
|
</td>
|
||||||
|
<td style={{textAlign: "right", display: "none"}} className={"total"}>
|
||||||
|
{ numberFormat.format(category.grandTotal) }
|
||||||
|
</td>
|
||||||
|
{ renderMonths(category) }
|
||||||
|
</tr>
|
||||||
|
<tr key={key + "-2"} style={{display: "none"}}>
|
||||||
|
<td colSpan={15}>
|
||||||
|
<table className={"detail"}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{textAlign: "left"}}>ID</th>
|
||||||
|
<th style={{textAlign: "left"}}>Source</th>
|
||||||
|
<th style={{textAlign: "left"}}>Description</th>
|
||||||
|
<th style={{textAlign: "left"}}>Date</th>
|
||||||
|
<th style={{textAlign: "right"}}>Amount</th>
|
||||||
|
<th style={{textAlign: "left"}}>Extra Description</th>
|
||||||
|
<th style={{textAlign: "left"}}>Pattern</th>
|
||||||
|
<th style={{textAlign: "left"}}>Flags</th>
|
||||||
|
<th style={{textAlign: "left"}}>Required Source</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ renderDetail(key, category) }
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
for(let childCategory of category.childCategories) {
|
||||||
|
result.push(renderCategory(level + 1, year, childCategory));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toReport = (years, startDate, endDate, includes) => {
|
||||||
|
console.log("toReport", years, startDate, endDate, includes);
|
||||||
|
navigate('/report-params', {replace: true, state: {
|
||||||
|
years: years,
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
includes: includes
|
||||||
|
}});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showColumn = (e, name, doNotCheckAll) => {
|
||||||
|
let cols = document.body.querySelectorAll("table.report td." + name + ",th." + name);
|
||||||
|
for(let col of cols) {
|
||||||
|
col.style.display = e.target.checked ? "" : "none";
|
||||||
|
}
|
||||||
|
if(!doNotCheckAll) {
|
||||||
|
let inputs = document.body.querySelectorAll("table.parameters label input:not(.all)");
|
||||||
|
var allChecked = true;
|
||||||
|
for(let input of inputs) {
|
||||||
|
if(!input.checked) {
|
||||||
|
allChecked = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let input = document.body.querySelector("table.parameters label input.all");
|
||||||
|
input.checked = allChecked;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let input = document.body.querySelector("table.parameters label input." + name);
|
||||||
|
input.checked = e.target.checked;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAllColumns = (e) => {
|
||||||
|
showColumn(e, "average", true);
|
||||||
|
showColumn(e, "total", true);
|
||||||
|
showColumn(e, "month0", true);
|
||||||
|
showColumn(e, "month1", true);
|
||||||
|
showColumn(e, "month2", true);
|
||||||
|
showColumn(e, "month3", true);
|
||||||
|
showColumn(e, "month4", true);
|
||||||
|
showColumn(e, "month5", true);
|
||||||
|
showColumn(e, "month6", true);
|
||||||
|
showColumn(e, "month7", true);
|
||||||
|
showColumn(e, "month8", true);
|
||||||
|
showColumn(e, "month9", true);
|
||||||
|
showColumn(e, "month10", true);
|
||||||
|
showColumn(e, "month11", true);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("report render", report);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Report</h1>
|
||||||
|
<table className={"parameters"}>
|
||||||
|
<thead>
|
||||||
|
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Years</td>
|
||||||
|
<td>{years}</td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "average");}} className={"average"}/> Average</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month0");}} className={"month0"}/> Jan</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month4");}} className={"month4"}/> May</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month8");}} className={"month8"}/> Sep</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showInputs(e)}}/>Input Budgets</label></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Start date</td>
|
||||||
|
<td>{startDate}</td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "total");}} className={"total"}/> Total</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month1");}} className={"month1"}/> Feb</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month5");}} className={"month5"}/> Jun</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month9");}} className={"month9"}/> Oct</label></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>End date</td>
|
||||||
|
<td>{endDate}</td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showAllColumns(e);}} className={"all"}/> All</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month2");}} className={"month2"}/> Mar</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month6");}} className={"month6"}/> Jul</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month10");}} className={"month10"}/> Nov</label></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Includes</td>
|
||||||
|
<td>{includes}</td>
|
||||||
|
<td/>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month3");}} className={"month3"}/> Apr</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month7");}} className={"month7"}/> Aug</label></td>
|
||||||
|
<td><label><input type="checkbox" onClick={(e) => {showColumn(e, "month11");}} className={"month11"}/> Dec</label></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div>
|
||||||
|
<a href="/report-params" onClick={() => {toReport(years, startDate, endDate, includes)}}>Rerun</a>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
Object.values(reportMap).map(yearReport => {
|
||||||
|
let style = {textAlign: "right", display: "none"};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2 key={"key-h2-" + yearReport.year}>{yearReport.year}</h2>
|
||||||
|
<table key={"key-table-" + yearReport.year} className={"report"}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{textAlign: left}}>Category</th>
|
||||||
|
<th style={style} className={"average"}>Average</th>
|
||||||
|
<th style={style} className={"total"}>Total</th>
|
||||||
|
<th style={style} className={"month0"}>Jan</th>
|
||||||
|
<th style={style} className={"month1"}>Feb</th>
|
||||||
|
<th style={style} className={"month2"}>Mar</th>
|
||||||
|
<th style={style} className={"month3"}>Apr</th>
|
||||||
|
<th style={style} className={"month4"}>May</th>
|
||||||
|
<th style={style} className={"month5"}>Jun</th>
|
||||||
|
<th style={style} className={"month6"}>Jul</th>
|
||||||
|
<th style={style} className={"month7"}>Aug</th>
|
||||||
|
<th style={style} className={"month8"}>Sep</th>
|
||||||
|
<th style={style} className={"month9"}>Oct</th>
|
||||||
|
<th style={style} className={"month10"}>Nov</th>
|
||||||
|
<th style={style} className={"month11"}>Dec</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ renderCategory(0, yearReport.year, yearReport.rootCategory) }
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Report;
|
||||||
98
src/ReportParams.jsx
Normal file
98
src/ReportParams.jsx
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useState, useEffect} from "react";
|
||||||
|
import {useSearchParams, useNavigate} from 'react-router-dom';
|
||||||
|
|
||||||
|
const ReportParamsForm = (props) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [inputs, setInputs] = useState({});
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
console.log("ReportParamsForm: calling useEffect");
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("ReportParamsForm: useEffect");
|
||||||
|
console.log("searchParams", searchParams);
|
||||||
|
let years = searchParams.get("years") || window.localStorage.getItem('report-years');
|
||||||
|
let startDate = searchParams.get("startDate") || window.localStorage.getItem('report-startDate');
|
||||||
|
let endDate = searchParams.get("endDate") || window.localStorage.getItem('report-endDate');
|
||||||
|
let includes = searchParams.get("includes") || window.localStorage.getItem('report-includes');
|
||||||
|
let tmpInputs = {};
|
||||||
|
if(years) {
|
||||||
|
tmpInputs.years = years == "null" ? null : years;
|
||||||
|
}
|
||||||
|
if(startDate) {
|
||||||
|
tmpInputs.startDate = startDate == 'null' ? null : startDate;
|
||||||
|
}
|
||||||
|
if(endDate) {
|
||||||
|
tmpInputs.endDate = endDate == 'null' ? null : endDate;
|
||||||
|
}
|
||||||
|
if(includes) {
|
||||||
|
tmpInputs.includes = includes == 'null' ? null : includes;
|
||||||
|
}
|
||||||
|
setInputs(tmpInputs);
|
||||||
|
}, [navigate, searchParams]);
|
||||||
|
|
||||||
|
const handleChange = event => {
|
||||||
|
const name = event.target.name;
|
||||||
|
const value = event.target.value;
|
||||||
|
setInputs(values => ({...values, [name]: value}));
|
||||||
|
console.log("handleChange", inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log("handleSubmit", inputs);
|
||||||
|
window.localStorage.setItem('report-years', inputs.years);
|
||||||
|
window.localStorage.setItem('report-startDate', inputs.startDate);
|
||||||
|
window.localStorage.setItem('report-endDate', inputs.endDate);
|
||||||
|
window.localStorage.setItem('report-includes', inputs.includes);
|
||||||
|
navigate('/report',{state:inputs});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("ReportParamsForm: render, inputs", inputs);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form name="report-params" onSubmit={handleSubmit}>
|
||||||
|
<h1>Report Parameters</h1>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><label htmlFor="years">Years:</label></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="years" id="years"
|
||||||
|
value={inputs.years || ""}
|
||||||
|
onChange={handleChange} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label htmlFor="startDate">Start date:</label></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="startDate" id="startDate"
|
||||||
|
value={inputs.startDate || ""}
|
||||||
|
onChange={handleChange} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label htmlFor="endDate">End date:</label></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="endDate" id="endDate"
|
||||||
|
value={inputs.endDate || ""}
|
||||||
|
onChange={handleChange} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label htmlFor="includes">Includes:</label></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="includes" id="includes"
|
||||||
|
value={inputs.includes || ""}
|
||||||
|
onChange={handleChange} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<input type="submit"/>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReportParamsForm;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const baseUrl = 'http://localhost:9090';
|
const baseUrl = 'http://kirk:9090';
|
||||||
|
|
||||||
const fetchOptions = (method, data) => {
|
const fetchOptions = (method, data) => {
|
||||||
let token = window.localStorage.getItem('token');
|
let token = window.localStorage.getItem('token');
|
||||||
|
|
@ -76,6 +76,52 @@ export const getRegexesBySource = source => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const categorize = (redo) => {
|
||||||
|
let url = redo ? `${baseUrl}/categorize` : `${baseUrl}/problems`;
|
||||||
|
return fetch(url, fetchOptions('GET')).then(response => {
|
||||||
|
// console.log(url, "response", response);
|
||||||
|
if(response.status !== 200) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reject({status: response.status});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response.json(); // promise
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const report = (years, startDate, endDate, includes) => {
|
||||||
|
let url = `${baseUrl}/report`;
|
||||||
|
let params = {};
|
||||||
|
if(years) {
|
||||||
|
params.years = years;
|
||||||
|
}
|
||||||
|
if(startDate) {
|
||||||
|
params.startDate = startDate;
|
||||||
|
}
|
||||||
|
if(endDate) {
|
||||||
|
params.endDate = endDate;
|
||||||
|
}
|
||||||
|
if(includes) {
|
||||||
|
params.includes = includes;
|
||||||
|
}
|
||||||
|
var sep = "?";
|
||||||
|
for(let key in params) {
|
||||||
|
let value = params[key];
|
||||||
|
url += sep;
|
||||||
|
sep = "&";
|
||||||
|
url += key + "=" + value;
|
||||||
|
}
|
||||||
|
return fetch(url, fetchOptions('GET')).then(response => {
|
||||||
|
// console.log(url, "response", response);
|
||||||
|
if(response.status !== 200) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reject({status: response.status});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response.json(); // promise
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const getRegexes = () => {
|
export const getRegexes = () => {
|
||||||
let url = `${baseUrl}/regexes`;
|
let url = `${baseUrl}/regexes`;
|
||||||
return fetch(url, fetchOptions('GET')).then(response => {
|
return fetch(url, fetchOptions('GET')).then(response => {
|
||||||
|
|
@ -222,7 +268,21 @@ export const createCategory = (dataResult, parentCategoryId) => ({
|
||||||
name: dataResult.name
|
name: dataResult.name
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const alphabetize = (siblings) => {
|
||||||
|
siblings.sort(function(c1, c2) {
|
||||||
|
if(c1.name < c2.name) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if(c1.name > c2.name) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return siblings;
|
||||||
|
};
|
||||||
|
|
||||||
export const getCategoryAncestry = categoryId => {
|
export const getCategoryAncestry = categoryId => {
|
||||||
|
if(categoryId) {
|
||||||
let url = `${baseUrl}/categories/ancestry/${categoryId}`;
|
let url = `${baseUrl}/categories/ancestry/${categoryId}`;
|
||||||
return fetch(url, fetchOptions('GET')).then(response => {
|
return fetch(url, fetchOptions('GET')).then(response => {
|
||||||
// console.log(url, "response", response);
|
// console.log(url, "response", response);
|
||||||
|
|
@ -247,7 +307,7 @@ export const getCategoryAncestry = categoryId => {
|
||||||
let category = {parentCategoryId: lastCategoryId};
|
let category = {parentCategoryId: lastCategoryId};
|
||||||
getCategories(category.parentCategoryId).then(siblings => {
|
getCategories(category.parentCategoryId).then(siblings => {
|
||||||
// console.log("siblings", siblings);
|
// console.log("siblings", siblings);
|
||||||
category.siblings = siblings;
|
category.siblings = alphabetize(siblings);
|
||||||
ancestry.push(category);
|
ancestry.push(category);
|
||||||
resolve(ancestry);
|
resolve(ancestry);
|
||||||
});
|
});
|
||||||
|
|
@ -257,7 +317,7 @@ export const getCategoryAncestry = categoryId => {
|
||||||
category.siblings = [];
|
category.siblings = [];
|
||||||
getCategories(category.parentCategoryId).then(siblings => {
|
getCategories(category.parentCategoryId).then(siblings => {
|
||||||
// console.log("siblings", siblings);
|
// console.log("siblings", siblings);
|
||||||
category.siblings = siblings;
|
category.siblings = alphabetize(siblings);
|
||||||
getSelectionList(index + 1);
|
getSelectionList(index + 1);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -268,6 +328,19 @@ export const getCategoryAncestry = categoryId => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let category = {};
|
||||||
|
getCategories(category.parentCategoryId).then(siblings => {
|
||||||
|
// console.log("siblings", siblings);
|
||||||
|
category.siblings = alphabetize(siblings);
|
||||||
|
let ancestry = [category];
|
||||||
|
resolve(ancestry);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveCategory = category => {
|
export const saveCategory = category => {
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,20 @@ import LoginForm from './LoginForm';
|
||||||
import {BrowserRouter, Routes, Route, Link} from "react-router-dom";
|
import {BrowserRouter, Routes, Route, Link} from "react-router-dom";
|
||||||
import Regexes from "./Regexes";
|
import Regexes from "./Regexes";
|
||||||
import EditRegex from "./EditRegex";
|
import EditRegex from "./EditRegex";
|
||||||
|
import Categorize from "./Categorize";
|
||||||
|
import Report from "./Report";
|
||||||
|
import ReportParamsForm from "./ReportParams";
|
||||||
import './main.css';
|
import './main.css';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
let token = window.localStorage.getItem('token');
|
let token = window.localStorage.getItem('token');
|
||||||
console.log('token', token);
|
console.log('App: token', token);
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Link to="/">Regexes</Link>
|
<Link to="/">Regexes</Link>
|
||||||
<Link to="/week">Week</Link>
|
<Link to="/Categorize/true">Categorize</Link>
|
||||||
|
<Link to="/Categorize/false">Problems</Link>
|
||||||
|
<Link to="/report-params">Report</Link>
|
||||||
<Link to="/login">Log out</Link>
|
<Link to="/login">Log out</Link>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
@ -20,6 +25,9 @@ export default function App() {
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route exact path="/regexes/:categoryParam" element={<Regexes/>}/>
|
<Route exact path="/regexes/:categoryParam" element={<Regexes/>}/>
|
||||||
<Route exact path="/regexes" element={<Regexes/>}/>
|
<Route exact path="/regexes" element={<Regexes/>}/>
|
||||||
|
<Route exact path="/categorize/:redo" element={<Categorize/>}/>
|
||||||
|
<Route exact path="/report" element={<Report/>}/>
|
||||||
|
<Route exact path="/report-params" element={<ReportParamsForm/>}/>
|
||||||
<Route exact path="/regex/:regexIdParam" element={<EditRegex/>}/>
|
<Route exact path="/regex/:regexIdParam" element={<EditRegex/>}/>
|
||||||
<Route exact path="/regex" element={<EditRegex/>}/>
|
<Route exact path="/regex" element={<EditRegex/>}/>
|
||||||
<Route exact path="/login" element={<LoginForm />}/>
|
<Route exact path="/login" element={<LoginForm />}/>
|
||||||
|
|
|
||||||
15
src/main.css
15
src/main.css
|
|
@ -2,6 +2,18 @@ body {
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
table.mat tr td {
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
}
|
||||||
|
table.ut tr td {
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
}
|
||||||
|
table.mat tr td.amount {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
table.ut tr td.amount {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
table.week tr td.duration {
|
table.week tr td.duration {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
@ -24,6 +36,9 @@ div.header-date, div.header-links {
|
||||||
div.header-date {
|
div.header-date {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
table.detail tr th,td {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
@media only screen and (max-width: 640px) {
|
@media only screen and (max-width: 640px) {
|
||||||
body {
|
body {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|
|
||||||
2
start
2
start
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
npm run build > build.log 2> build.err.log
|
npm run build > build.log 2> build.err.log
|
||||||
serve -s --no-port-switching -l 3000 build > start.log 2> start.err.log &
|
serve -s --no-port-switching -l 3001 build > start.log 2> start.err.log &
|
||||||
echo "$!" > pid
|
echo "$!" > pid
|
||||||
Loading…
Reference in a new issue