Add /categorize, /problems and /report

This commit is contained in:
Steve Schafer 2026-01-17 09:59:06 -07:00
parent 63f36a7ab0
commit 8082db19ac
9 changed files with 891 additions and 83 deletions

228
src/Categorize.jsx Normal file
View 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;

View file

@ -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 (

View file

@ -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
View 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
View 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;

View file

@ -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 => {

View file

@ -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> &nbsp; <Link to="/">Regexes</Link> &nbsp;
<Link to="/week">Week</Link> &nbsp; <Link to="/Categorize/true">Categorize</Link> &nbsp;
<Link to="/Categorize/false">Problems</Link> &nbsp;
<Link to="/report-params">Report</Link> &nbsp;
<Link to="/login">Log out</Link> &nbsp; <Link to="/login">Log out</Link> &nbsp;
<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 />}/>

View file

@ -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
View file

@ -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