Skip to content
Snippets Groups Projects
Commit 846ac73e authored by Recolic's avatar Recolic :house_with_garden:
Browse files

.

parents
No related branches found
No related tags found
No related merge requests found
# recolic's budget tracking tool
recolic自用的记账工具
## Installation / Deployment
1. Download all files into any PHP-enabled HTTP server.
2. (Optional) Create an 'Spec Budget' excel on Google Drive or OneDrive and share to everyone, and put link into `spec_url.txt`.
3. Enjoy.
## 安装方法
1. 把所有文件下载到一个有PHP的HTTP服务器。
2. (可选) 创建一个 '意外支出' 的excel文档在Google Drive或OneDrive里面,分享给匿名用户,并将连结放进`spec_url.txt`
3. 使用它。
## How does it work
认识数位版账本前, 首先你要了解它的前身, 手写budget tracking是如何work的.
![](./NextDocument.jpg) ![](../NextDocument.jpg)
- 如何记账
在日历上, 每天都会有一定的budget, 例如\$15. 每当你产生一笔消费, 就在日历上划去对应的天数. 例如你消费了45\$, 就需要划去三天, 表示这三天的预算被用掉了.
如果消费不能被budget整除, 例如我消费6\$买了牛肉, 不满15\$, 就在空白处写下"6"表示有6\$的消费还没被统计. 假如下次我又消费10\$买了寿司, 我就可以与之前的6\$结合起来, 划掉之前的6\$和一天的预算(15\$), 再把剩余的1\$写在旁边.
假如我有10\$消费被退款了, 但我们的表格无法回退. 没关系, 我们只需要记下"-10\$"即可, 以后消费时自然会拿来结合.
- 如何评估
现在我们已经把消费记下了, 但是如何评估我们的财务状况呢? 很简单. 只需要将日历上今天的日期, 与你已经用掉的预算日对比即可.
例如: 今天是6月22日, 但你已经划掉了6月26日的budget. 这意味着你的财务超支了, 应当考虑缩减开支.
- 特殊支出
有些支出并不是由我们的每日预算支付的. 例如: 你的房租, 水电费用, 电话费网费, 旅行机票消费, 大件数码产品. 它们有的属于"不可避免的周期性支出", 另一些属于"特事特办的意外性支出".
对于"不可避免的周期性支出", 我会经过一次审核, 即自动扣款, 不再计入日常预算. (显然此类支出即使计入预算也是毫无意义的); 对于"特事特办的意外性支出", 我会进行专项审核, 然后记录在日历旁边.
- 意外状况
有时我们超支了太多, 下定决心进行债务重整. 有时我们认为预算额度不合适, 决定对预算额度进行调整. 有时我们刚刚旅行归来, 在日历上留下了大片的空白.
这时候我们可以重新选定一个开始日期, 用相同或不同的额度, 开启一个新的预算周期. 这在手写账本上表现为, 很多日期被框起来, 整个划掉. 而数位版日历也具有这个功能.
- 数位化
在充分理解手写账本如何工作后, 数位版以完全相同的逻辑工作, 只是, 你不必用纸笔进行记录和计算, 而是由后端自动处理.
日历上用深色表示相应日期的预算已被使用, 而浅色表示相应日期的预算尚未使用. 你只需输入你的消费金额并提交, 而不满一天的消费会被记住并自动统计.
当你遭遇*意外情况*, 希望开启新的预算周期时, 只需点击`Start a new budget plan`, 输入相关信息即可.
> 注意! 在你开始使用前, 别忘了先创建一个预算周期哦.
- corner case: 开支何时记入系统
开支于具确定数目后之最近合理时刻记入系统。
可能发生之部分退款,视为数目之未确定。
可能发生之全额退款,视为数目已确定。
举例:Walmart已下单预付款,然配送时可能根据实际货品重量、缺货情况进行部分价格调整。此情况下应待配送完成后(即开支之数目确定后)将所确定之数目入账。
Amazon下单后亦存在完全退款之可能,然下单时开支数目已于合理标准内几近确定,故可作为入账之根据。
- corner case: 现金等价物
例如你可能消费了航空里程、信用卡点数、信用卡福利、商户credit,你消费的并不是现金,虽然这些等价物是曾经用现金购买的。
我们只在支付现金(以获取这些等价物)时记录一次开销,而消费这些现金等价物时不再另行记录。
这是因为现金等价物之实际价值难以评估,甚至可能并非来自现金购买。而我们只对现金开销之管控感兴趣。若将现金等价物视为现金,则会导致重复记录开销(想想存款准备金率如何影响货币总量)。
## Deployment
Put `release/` into any php-enabled web server, do `chmod 777 -R deploy_dir`, and enjoy.
api.php 0 → 100644
<?php
function newCost($date, $cost) {
$file = 'costs.txt';
$data = "$date,$cost\n";
file_put_contents($file, $data, FILE_APPEND);
}
function newBudgetInfo($startDate, $budgetPerDay) {
$file = 'budget_info.txt';
$data = "$startDate,$budgetPerDay\n";
file_put_contents($file, $data, FILE_APPEND);
}
function queryCurrentBudget($todayDate) {
// Read and parse the file
$filename = 'budget_info.txt';
$lines = file($filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$budgets = array_map('str_getcsv', $lines);
// Sort the array by the date (first element of each sub-array)
usort($budgets, function($a, $b) { return strcmp($a[0], $b[0]); });
// Determine the active budget
$activeBudget = "[N/A]";
foreach ($budgets as $budget) {
if ($todayDate >= $budget[0]) {
$activeBudget = $budget[1];
} else {
break;
}
}
return $activeBudget;
}
function renderPage() {
$costsFile = 'costs.txt';
$budgetFile = 'budget_info.txt';
if (!file_exists($costsFile) || !file_exists($budgetFile)) {
return [];
}
$costs = array_map('str_getcsv', file($costsFile));
$budgets = array_map('str_getcsv', file($budgetFile));
usort($costs, function($a, $b) {
return strtotime($a[0]) - strtotime($b[0]);
});
usort($budgets, function($a, $b) {
return strtotime($a[0]) - strtotime($b[0]);
});
$results = [];
$budgetIndex = 0;
$usedBudget = 0;
$currentBudgetInfo = $budgets[$budgetIndex];
$currentStartDate = $currentBudgetInfo[0];
$currentBudgetPerDay = $currentBudgetInfo[1];
if ($budgetIndex < count($budgets) - 1)
$nextBudgetStartingDate = $budgets[$budgetIndex + 1][0];
else
$nextBudgetStartingDate = "2099-01-01";
foreach ($costs as $cost) {
$costDate = $cost[0];
$costValue = $cost[1];
// Check if cost is out of first budget ranges
if ($budgetIndex >= count($budgets) || strtotime($costDate) < strtotime($currentStartDate)) {
echo "Debug: Cost $costValue on $costDate is out of budget range. Did you forget to define budget?\n";
continue;
}
// Move to the next budget range if the cost date is beyond the current range
while (strtotime($costDate) >= strtotime($nextBudgetStartingDate)) {
echo "DEBUG: summary: usedBud=$usedBudget, currBud/d=$currentBudgetPerDay, currentStartDate=$currentStartDate, nextStartDate=$nextBudgetStartingDate\n";
$daysUsed = ceil($usedBudget / $currentBudgetPerDay);
for ($i = 0; $i < $daysUsed; $i++) {
$whichDayBudgetBeingUsed = strtotime("+$i days", strtotime($currentStartDate));
if ($whichDayBudgetBeingUsed >= strtotime($nextBudgetStartingDate)) {
$uncoveredCost = $usedBudget - $currentBudgetPerDay * $i;
echo "Debug: Discarded $uncoveredCost at $nextBudgetStartingDate to start next budget range \n";
break;
}
$results[] = date('Y-m-d', $whichDayBudgetBeingUsed);
}
$usedBudget = 0;
$budgetIndex++;
$currentBudgetInfo = $budgets[$budgetIndex];
$currentStartDate = $currentBudgetInfo[0];
$currentBudgetPerDay = $currentBudgetInfo[1];
if ($budgetIndex < count($budgets) - 1)
$nextBudgetStartingDate = $budgets[$budgetIndex + 1][0];
else
$nextBudgetStartingDate = "2099-01-01";
}
// Add cost to the current budget
$usedBudget += $costValue;
}
// Handle the remaining budget usage for the last budget range.
echo "DEBUG:Tsummary: usedBud=$usedBudget, currBud/d=$currentBudgetPerDay, currentStartDate=$currentStartDate, nextStartDate=$nextBudgetStartingDate\n";
$daysUsed = ceil($usedBudget / $currentBudgetPerDay);
for ($i = 0; $i < $daysUsed; $i++) {
$whichDayBudgetBeingUsed = strtotime("+$i days", strtotime($currentStartDate));
if ($whichDayBudgetBeingUsed >= strtotime($nextBudgetStartingDate)) {
$uncoveredCost = $usedBudget - $currentBudgetPerDay * $i;
echo "Debug: Discarded $uncoveredCost at $nextBudgetStartingDate to start next budget range \n";
break;
}
$results[] = date('Y-m-d', $whichDayBudgetBeingUsed);
}
return $results;
}
// function testRenderPage() {
// // Clean up previous test data
// // @unlink('costs.txt');
// // @unlink('budget_info.txt');
//
// // Add some test data
// // newBudgetInfo('2024-06-01', 30);
// // newCost('2024-06-01', 50);
// // newCost('2024-06-01', 75);
// // newCost('2024-06-01', 30);
// // newCost('2024-06-01', 40);
// //
// // newBudgetInfo('2024-06-04', 100);
// // newCost('2024-06-06', 30);
// // newCost('2024-06-06', 40);
// // newCost('2024-06-07', 140);
//
// // Get the result from renderPage
// $result = renderPage();
//
// // Print the result
// foreach ($result as $date) {
// echo "Budget used on: $date\n";
// }
// }
//
// testRenderPage();
// Handling incoming requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['cost']) && isset($_POST['date'])) {
// Handle new cost entry
$cost = $_POST['cost'];
$date = $_POST['date'];
newCost($date, $cost);
echo "New cost added: $cost on $date\n";
} elseif (isset($_POST['daily_budget']) && isset($_POST['starting_date'])) {
// Handle new budget info entry
$dailyBudget = $_POST['daily_budget'];
$startingDate = $_POST['starting_date'];
newBudgetInfo($startingDate, $dailyBudget);
echo "New budget info added: $dailyBudget per day starting from $startingDate\n";
} elseif (isset($_POST['query_budget_for_date'])) {
$date = $_POST['query_budget_for_date'];
$res = queryCurrentBudget($date);
echo "$res";
} elseif (isset($_POST['query_spec_url'])) {
$res = file_get_contents("spec_url.txt");
if ($res === false) {echo "read spec_url.txt failed";http_response_code(500);}
else {echo "$res";}
} elseif (isset($_POST['query_latest_tx'])) {
$lines = file("costs.txt", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$lastLines = array_slice($lines, -4);
foreach (array_reverse($lastLines) as $line) { echo $line . PHP_EOL; }
} else {
echo "Invalid POST request\n";
}
} elseif ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Handle render page request
$results = renderPage();
foreach ($results as $date) {
echo "Budget used on: $date\n";
}
} else {
echo "Invalid request method\n";
}
?>
source diff could not be displayed: it is too large. Options to address this: view the blob.
.cjslib-calendar {
/*width: 800px;*/
max-width: 1200px;
height: 800px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
font-family: "Satellite", "Roboto", sans-serif;
border: 1px solid rgba(21, 21, 21, 0.12);
-webkit-transform: scale(1);
-ms-transform: scale(1);
transform: scale(1);
-webkit-box-shadow: 0px 0px 4px rgba(21, 21, 21, 0.21);
box-shadow: 0px 0px 4px rgba(21, 21, 21, 0.21);
-ms-user-select: none;
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
}
.cjslib-calendar.cjslib-size-small {
/*width: 400px;*/
max-width: 600px;
height: 400px;
}
.cjslib-calendar.cjslib-size-medium {
width: 600px;
height: 600px;
}
.cjslib-calendar.cjslib-size-large {
width: 800px;
height: 800px;
}
.cjslib-year {
width: calc(100%);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
padding: 5px;
font-size: 14px;
}
.cjslib-year > span {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-line-pack: center;
align-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-transform: uppercase;
}
.cjslib-year > div {
cursor: pointer;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-ms-flex-line-pack: center;
align-content: center;
}
.cjslib-month {
z-index: 1;
width: calc(100%);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
padding: 5px 5px; /*recolic: made month smaller*/
font-size: 24px; /*recolic: made month smaller*/
-webkit-box-shadow: 0px 1px 4px rgba(21, 21, 21, 0.12);
box-shadow: 0px 1px 4px rgba(21, 21, 21, 0.12);
}
.cjslib-month > span {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-line-pack: center;
align-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-transform: uppercase;
}
.cjslib-month > div {
cursor: pointer;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-ms-flex-line-pack: center;
align-content: center;
}
.cjslib-labels {
width: 100%;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
}
.cjslib-labels > span {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
font-size: 12px;
text-transform: uppercase;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-line-pack: center;
align-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
padding: 10px;
}
.cjslib-days {
background-color: #F6F6F6;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-shadow: 0px 2px 6px -2px rgba(21, 21, 21, 0.21);
box-shadow: 0px 2px 6px -2px rgba(21, 21, 21, 0.21);
}
.cjslib-row {
width: 100%;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
}
.cjslib-day {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
padding: 5px;
cursor: pointer;
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
border-bottom: 1px solid rgba(21, 21, 21, .12);
border-right: 1px solid rgba(21, 21, 21, .12);
-webkit-transition: box-shadow 200ms ease-in-out;
-webkit-transition: -webkit-box-shadow 200ms ease-in-out;
transition: -webkit-box-shadow 200ms ease-in-out;
-o-transition: box-shadow 200ms ease-in-out;
transition: box-shadow 200ms ease-in-out;
transition: box-shadow 200ms ease-in-out, -webkit-box-shadow 200ms ease-in-out;
}
.cjslib-day:last-child {
border-right: none;
}
.cjslib-day:hover {
background-color: rgba(21, 21, 21, 0.012);
-webkit-box-shadow: inset 0px 0px 4px rgba(21, 21, 21, 0.21);
box-shadow: inset 0px 0px 4px rgba(21, 21, 21, 0.21);
}
.cjslib-day-radios {
display: none;
}
.cjslib-day-radios:checked+.cjslib-day {
background-color: rgba(21, 21, 21, 0.012);
-webkit-box-shadow: inset 0px 0px 4px rgba(21, 21, 21, 0.21);
box-shadow: inset 0px 0px 4px rgba(21, 21, 21, 0.21);
}
.cjslib-day > .cjslib-day-num {
width: auto;
height: -webkit-fit-content;
height: -moz-fit-content;
height: fit-content;
font-size: 14px;
color: rgba(21, 21, 21, 0.84);
}
.cjslib-day.cjslib-day-today > .cjslib-day-num {
padding-bottom: 3px;
border-bottom: 2px solid;
border-radius: 1px;
}
.cjslib-day > .cjslib-day-indicator {
font-size: 0px;
position: absolute;
border-radius: 100%;
-webkit-box-shadow: 0px 2px 4px rgba(21, 21, 21, 0.21);
box-shadow: 0px 2px 4px rgba(21, 21, 21, 0.21);
}
.cjslib-indicator-type-numeric {
padding: 3px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
}
.cjslib-day.cjslib-day-diluted {
background-color: rgba(21, 21, 21, 0.021);
-webkit-box-shadow: inset 0px 0px 1px rgba(21, 21, 21, 0.12);
box-shadow: inset 0px 0px 1px rgba(21, 21, 21, 0.12);
}
.cjslib-day.cjslib-day-diluted > .cjslib-day-num {
width: auto;
font-size: 10px;
color: rgba(21, 21, 21, 0.73);
}
.cjslib-day-indicator:empty,
.cjslib-day.cjslib-day-diluted > .cjslib-day-indicator {
display: none !important;
}
.cjslib-calendar.cjslib-size-small .cjslib-day > .cjslib-day-indicator {
width: 8px;
height: 8px;
bottom: 7px;
right: 7px;
}
.cjslib-calendar.cjslib-size-small .cjslib-day > .cjslib-indicator-type-numeric {
font-size: 7px;
}
.cjslib-calendar.cjslib-size-small .cjslib-day > .cjslib-indicator-pos-top {
top: 7px;
bottom: unset;
}
.cjslib-calendar.cjslib-size-medium .cjslib-day > .cjslib-day-indicator {
width: 18px;
height: 18px;
bottom: 10px;
right: 10px;
}
.cjslib-calendar.cjslib-size-medium .cjslib-day > .cjslib-indicator-type-numeric {
font-size: 10px;
}
.cjslib-calendar.cjslib-size-medium .cjslib-day > .cjslib-indicator-pos-top {
top: 10px;
bottom: unset;
}
.cjslib-calendar.cjslib-size-large .cjslib-day > .cjslib-day-indicator {
width: 24px;
height: 24px;
bottom: 14px;
right: 14px;
}
.cjslib-calendar.cjslib-size-large .cjslib-day > .cjslib-indicator-type-numeric {
font-size: 12px;
}
.cjslib-calendar.cjslib-size-large .cjslib-day > .cjslib-indicator-pos-top {
top: 14px;
bottom: unset;
}
.cjslib-events {
width: 800px;
height: 800px;
font-family: "Satellite", "Roboto", sans-serif;
-webkit-box-shadow: 0px 0px 4px rgba(21, 21, 21, 0.21);
box-shadow: 0px 0px 4px rgba(21, 21, 21, 0.21);
border: 1px solid rgba(21, 21, 21, 0.12);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-ms-user-select: none;
user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
}
.cjslib-events.cjslib-size-small {
width: 400px;
height: 400px;
}
.cjslib-events.cjslib-size-medium {
width: 600px;
height: 600px;
}
.cjslib-events.cjslib-size-large {
width: 800px;
height: 800px;
}
.cjslib-date {
width: calc(100% - 10px);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
padding: 5px;
font-size: 14px;
}
.cjslib-date > span {
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-line-pack: center;
align-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-transform: uppercase;
}
.cjslib-date > div {
cursor: pointer;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-ms-flex-line-pack: center;
align-content: center;
}
.cjslib-rows {
background-color: #F6F6F6;
width: 100%;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
overflow: auto !important;
}
.cjslib-list {
width: 100%;
position: relative;
overflow-y: visible !important;
padding: 0;
margin: 0;
color: rgba(21, 21, 21, 0.94);
padding-bottom: 15px;
}
.cjslib-list-history {
padding-top: 10px;
width: calc(100% - 20px);
margin-left: 10px;
margin-right: 10px;
}
.cjslib-list-history > .cjslib-list-history-title {
padding: 5px 0px;
border-radius: 2px;
}
.cjslib-list-placeholder {
height: 100%;
border: none !important;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-ms-flex-line-pack: center;
align-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
color: #757575;
pointer-events: none;
display: none;
}
.cjslib-list-placeholder * {
pointer-events: all;
}
.cjslib-list .cjslib-list-placeholder:only-child {
display: block !important;
}
.cjslib-list > li {
width: 100%;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
border-bottom: 1px solid rgba(21, 21, 21, 0.12);
}
.cjslib-list > li:hover {
-webkit-box-shadow: inset 0px 0px 4px rgba(21, 21, 21, 0.21);
box-shadow: inset 0px 0px 4px rgba(21, 21, 21, 0.21);
}
.cjslib-list > li > div {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 2;
-ms-flex: 2;
flex: 2;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-ms-flex-line-pack: center;
align-content: center;
padding: 10px;
border-right: 1px solid rgba(21, 21, 21, 0.12);
}
.cjslib-time {
font-size: 14px;
}
.cjslib-m {
font-size: 14px;
text-transform: uppercase;
padding-left: 5px;
}
.cjslib-list > li > p {
-webkit-box-flex: 4;
-ms-flex: 4;
flex: 4;
margin: 10px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-ms-flex-line-pack: center;
align-content: center;
font-size: 18px;
word-wrap: break-word;
word-break: break-word;
}
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Budget.Cal</title>
<link rel="stylesheet" href="bulma.min.css">
<link rel="stylesheet" href="calendarorganizer.css" />
<style>
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
</style>
</head>
<body>
<section class="section">
<div class="container">
<form id="submit-form" onSubmit="submitNewCostForm(); return false;">
<label class="label" id="notice-label">Daily Budget Tracking</label>
<div class="field has-addons">
<div class="control">
<input class="input is-large" type="number" id="cost-input" placeholder="Enter cost">
</div>
<div class="control">
<button class="button is-primary is-large" id="submit-btn">Submit</button>
</div>
</div>
</form>
</div>
<br />
<div class="container">
<div id="calendarContainer"></div>
<div id="organizerContainer" style="display: none;"></div>
</div>
<br />
<div class="container">
<a href="" id="url-spec-budget"></a> |
<a id='new-budget'>Start a new budget plan</a>
<br />
<p id="history-cost"></p>
<br />
<a href="https://git.recolic.net/root/daily-scripts/-/blob/one/budget-cal/release/README.md">Help: Usage Guide</a>
</div>
</section>
<script src="calendarorganizer.js"></script>
<script>
const date_today = new Date().toISOString().split('T')[0]; // YYYY-mm-dd
// document.getElementById('submit-form').addEventListener('submit',
function submitNewCostForm() {
const cost = document.getElementById('cost-input').value;
document.getElementById('submit-btn').classList.add('loading');
const xhr = new XMLHttpRequest();
xhr.open('POST', "api.php", true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('POST request successful');
document.getElementById('cost-input').value = "";
location.reload(); // refresh current page
} else {
alert("HTTP POST request failed.");
}
}
};
xhr.send(`cost=${cost}&date=${date_today}`);
}
document.getElementById('new-budget').addEventListener('click', function () {
// Prompt the user for daily budget and starting date
const dailyBudget = prompt('Enter Daily Budget:', '');
if (dailyBudget == "" || !dailyBudget) return;
const startingDate = prompt('Enter Starting Date:', date_today);
if (!startingDate) return;
if (! /^\d{4}-\d{2}-\d{2}$/.test(startingDate)) {
alert("invalid starting date format.");
return;
}
const xhr = new XMLHttpRequest();
xhr.open('POST', "api.php", true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('POST request successful');
location.reload(); // refresh current page
} else {
alert("HTTP POST request failed.");
}
}
};
xhr.send(`daily_budget=${dailyBudget}&starting_date=${startingDate}`);
});
function refreshBudgetLabel() {
const xhr = new XMLHttpRequest();
xhr.open('POST', "api.php", true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('POST request successful');
if (xhr.responseText.length < 32) {
document.getElementById('notice-label').innerText = "Current Budget: $" + xhr.responseText + " /d";
}
} else {
console.log('POST query_budget_for_date API failed. common for local test');
}
}
};
xhr.send(`query_budget_for_date=${date_today}`);
}
function refreshSpecUrl() {
const xhr = new XMLHttpRequest();
xhr.open('POST', "api.php", true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('POST request successful');
document.getElementById('url-spec-budget').innerHTML = "SPEC Budget Tracking";
document.getElementById('url-spec-budget').href = xhr.responseText;
} else {
console.log('POST query_budget_for_date API failed. common for local test');
}
}
};
xhr.send(`query_spec_url=1`);
}
function refreshHistory() {
const xhr = new XMLHttpRequest();
xhr.open('POST', "api.php", true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('POST request successful');
document.getElementById('history-cost').innerHTML =
"Recent Tx: <br>" + xhr.responseText.replace(/\n/g, '<br>');
} else {
console.log('POST query_budget_for_date API failed. common for local test');
}
}
};
xhr.send(`query_latest_tx=1`);
}
function drawCalendar(budgetDic) {
var calendar = new Calendar("calendarContainer", "small",
[ "Sunday", 3 ],
[ "#ffc107", "#ffa000", "#ffffff", "#ffecb3" ],
{
days: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ],
months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ],
indicator:true,
indicator_type: 9// recolic customized indicator_type, indicates this day's budget used.
//placeholder: "<span>Custom Placeholder</span>"
});
var organizer = new Organizer("organizerContainer", calendar, budgetDic);
}
function downloadBudgetData() {
fetch('api.php')
.then(response => response.text())
.then(data => {
// Split the data into lines
const lines = data.trim().split('\n');
// Initialize the dictionary
const dictionary = {};
// Process each line
lines.forEach(line => {
try{
// console.log("DEBUG: line=" + line);
// Extract the date and text
const [_, _year, _month, _day] = /(\d{4})-(\d{2})-(\d{2})/.exec(line);
const year = Number(_year);
const month = Number(_month);
const day = Number(_day);
// Create the nested structure in the dictionary if needed
if (!dictionary[year]) {
dictionary[year] = {};
}
if (!dictionary[year][month]) {
dictionary[year][month] = {};
}
// Assign xxx to the corresponding date
dictionary[year][month][day] = [{startTime:"00:00",endTime:"24:00",text:"B"}];
} catch (error) {
// silent discard non-date line. console.error('Error processing line:', error);
}
});
drawCalendar(dictionary);
})
.catch(error => console.error('Error fetching data:', error));
}
// Draw the calendar on page load
// drawCalendar();
downloadBudgetData();
refreshBudgetLabel();
refreshSpecUrl();
refreshHistory();
</script>
</body>
</html>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment