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;
}
"use strict";
function Calendar(id, size, labelSettings, colors, options) {
this.id = id;
this.size = size;
this.labelSettings = labelSettings;
this.colors = colors;
this.initday = 0;
options = options || {};
this.indicator = true;
if (options.indicator != undefined) this.indicator = options.indicator;
this.indicator_type = 1;
if (options.indicator_type != undefined) this.indicator_type = options.indicator_type;
this.indicator_pos = (this.indicator_type == 1) ? "bottom" : "top";
if (options.indicator_pos != undefined) this.indicator_pos = options.indicator_pos;
var listPlaceholder = document.createElement("LI");
listPlaceholder.className = "cjslib-list-placeholder";
listPlaceholder.appendChild(document.createTextNode("No events on this day"));
listPlaceholder.style = "text-align: center; padding: 20px 0px;";
this.placeholder = listPlaceholder.outerHTML;
if (options.placeholder != undefined) this.placeholder = options.placeholder;
var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
if (options.months != undefined && options.months.length == 12) months = options.months;
var label = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
if (options.days != undefined && options.days.length == 7) label = options.days;
this.months = months;
this.defaultLabels = label;
this.label = [];
this.labels = [];
for (var i = 0; i < 7; i++) {
this.label.push(label[label.indexOf(labelSettings[0]) + this.label.length >= label.length ? Math.abs(label.length - (label.indexOf(labelSettings[0]) + this.label.length)) : label.indexOf(labelSettings[0]) + this.label.length]);
}
for (var i = 0; i < 7; i++) {
this.labels.push(this.label[i].substring(0, labelSettings[1] > 3 ? 3 : labelSettings[1]));
}
this.date = new Date();
this.today = new Date();
this.history = [];
this.draw();
this.update();
this.setOnClickListener('days-blocks');
this.setOnClickListener('month-slider');
this.setOnClickListener('year-slider');
}
Calendar.prototype = {
constructor: Calendar,
back: function back(func) {
var date = this.date;
var lastDay = new Date(date.getMonth() + 1 > 11 ? date.getFullYear() + 1 : date.getFullYear(), date.getMonth() + 1 > 12 ? 0 : date.getMonth() + 1, 0).getDate();
var previousLastDay = new Date(date.getMonth() < 0 ? date.getFullYear() - 1 : date.getFullYear(), date.getMonth() < 0 ? 11 : date.getMonth(), 0).getDate();
if (func == "month") {
if (date.getDate() > previousLastDay) {
this.changeDateTo(previousLastDay);
}
if (date.getMonth() > 0) date.setMonth(date.getMonth() - 1);else {
date.setMonth(11);
date.setFullYear(date.getFullYear() - 1);
}
} else if (func == "year") date.setFullYear(date.getFullYear() - 1);
this.update();
},
next: function next(func) {
var date = this.date;
var lastDay = new Date(date.getMonth() + 1 > 11 ? date.getFullYear() + 1 : date.getFullYear(), date.getMonth() + 1 > 12 ? 0 : date.getMonth() + 1, 0).getDate();
var soonLastDay = new Date(date.getMonth() + 2 > 11 ? date.getFullYear() + 1 : date.getFullYear(), date.getMonth() + 2 > 12 ? 0 : date.getMonth() + 2, 0).getDate();
if (func == "month") {
if (date.getDate() > soonLastDay) {
this.changeDateTo(soonLastDay);
}
if (date.getMonth() != 11) date.setMonth(date.getMonth() + 1);else {
date.setMonth(0);
date.setFullYear(date.getFullYear() + 1);
}
} else date.setFullYear(date.getFullYear() + 1);
this.update();
},
changeDateTo: function changeDateTo(theDay, theBlock) {
if (theBlock >= 31 && theDay <= 11 || theBlock <= 6 && theDay >= 8) {
if (theBlock >= 31 && theDay <= 11) this.next('month');else if (theBlock <= 6 && theDay >= 8) this.back('month');
this.date.setDate(theDay);
var calendarInstance = this;
setTimeout(function () {
calendarInstance.update();
}, 1);
return true;
} else this.date.setDate(theDay);
},
getDateString: function getDateString() {
return this.months[this.date.getMonth()] + " " + this.date.getDate() + ", " + this.date.getFullYear();
}
};
Calendar.prototype.draw = function () {
var backSvg = '<svg style="width: 24px; height: 24px;" viewBox="0 0 24 24"><path fill="' + this.colors[3] + '" d="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z"></path></svg>';
var nextSvg = '<svg style="width: 24px; height: 24px;" viewBox="0 0 24 24"><path fill="' + this.colors[3] + '" d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z"></path></svg>';
var theCalendar = document.createElement("DIV");
theCalendar.className = "cjslib-calendar cjslib-size-" + this.size;
document.getElementById(this.id).appendChild(theCalendar.cloneNode(true));
var theContainers = [],
theNames = ['year', 'month', 'labels', 'days'];
for (var i = 0; i < theNames.length; i++) {
theContainers[i] = document.createElement("DIV");
theContainers[i].className = "cjslib-" + theNames[i];
if (theNames[i] != "days") {
if (theNames[i] != "month") {
theContainers[i].style.backgroundColor = this.colors[1];
theContainers[i].style.color = this.colors[3];
if (theNames[i] != "labels") {
var backSlider = document.createElement("DIV");
backSlider.id = this.id + "-year-back";
backSlider.insertAdjacentHTML('beforeend', backSvg);
theContainers[i].appendChild(backSlider.cloneNode(true));
var theText = document.createElement("SPAN");
theText.id = this.id + "-" + theNames[i];
theContainers[i].appendChild(theText.cloneNode(true));
var nextSlider = document.createElement("DIV");
nextSlider.id = this.id + "-year-next";
nextSlider.insertAdjacentHTML('beforeend', nextSvg);
theContainers[i].appendChild(nextSlider.cloneNode(true));
}
} else {
theContainers[i].style.backgroundColor = this.colors[0];
theContainers[i].style.color = this.colors[2];
var backSlider = document.createElement("DIV");
backSlider.id = this.id + "-month-back";
backSlider.insertAdjacentHTML('beforeend', backSvg);
theContainers[i].appendChild(backSlider.cloneNode(true));
var theText = document.createElement("SPAN");
theText.id = this.id + "-" + theNames[i];
theContainers[i].appendChild(theText.cloneNode(true));
var nextSlider = document.createElement("DIV");
nextSlider.id = this.id + "-month-next";
nextSlider.insertAdjacentHTML('beforeend', nextSvg);
theContainers[i].appendChild(nextSlider.cloneNode(true));
}
}
}
for (var i = 0; i < this.labels.length; i++) {
var theLabel = document.createElement("SPAN");
theLabel.id = this.id + "-label-" + (i + 1);
theLabel.appendChild(document.createTextNode(this.labels[i]).cloneNode(true));
theContainers[2].appendChild(theLabel.cloneNode(true));
}
var theRows = [],
theDays = [],
theRadios = [];
for (var i = 0; i < 6; i++) {
theRows[i] = document.createElement("DIV");
theRows[i].className = "cjslib-row";
}
for (var i = 0, j = 0; i < 42; i++) {
theRadios[i] = document.createElement("INPUT");
theRadios[i].className = "cjslib-day-radios";
theRadios[i].type = "radio";
theRadios[i].name = this.id + "-day-radios";
theRadios[i].id = this.id + "-day-radio-" + (i + 1);
theDays[i] = document.createElement("LABEL");
theDays[i].className = "cjslib-day";
theDays[i].htmlFor = this.id + "-day-radio-" + (i + 1);
theDays[i].id = this.id + "-day-" + (i + 1);
var theText = document.createElement("SPAN");
theText.className = "cjslib-day-num";
theText.id = this.id + "-day-num-" + (i + 1);
theDays[i].appendChild(theText.cloneNode(true));
if (this.indicator) {
var theIndicator = document.createElement("SPAN");
theIndicator.className = "cjslib-day-indicator cjslib-indicator-pos-" + this.indicator_pos;
if (this.indicator_type == 1) theIndicator.className += " cjslib-indicator-type-numeric";
theIndicator.id = this.id + "-day-indicator-" + (i + 1);
theDays[i].appendChild(theIndicator.cloneNode(true));
}
theRows[j].appendChild(theRadios[i].cloneNode(true));
theRows[j].appendChild(theDays[i].cloneNode(true));
if ((i + 1) % 7 == 0) {
j++;
}
}
for (var i = 0; i < 6; i++) {
theContainers[3].appendChild(theRows[i].cloneNode(true));
}
for (var i = 0; i < theContainers.length; i++) {
theCalendar.appendChild(theContainers[i].cloneNode(true));
}
document.getElementById(this.id).innerHTML = "<style>.cjslib-day-indicator { color: " + this.colors[1] + "; background-color: " + this.colors[1] + "; } .cjslib-indicator-type-numeric { color: " + this.colors[2] + "; } .cjslib-day.cjslib-day-today > .cjslib-day-num { border-color: " + this.colors[1] + " !important; }</style>";
document.getElementById(this.id).appendChild(theCalendar.cloneNode(true));
};
Calendar.prototype.update = function () {
document.getElementById(this.id + '-year').innerHTML = this.date.getFullYear();
document.getElementById(this.id + '-month').innerHTML = this.months[this.date.getMonth()];
for (var i = 1; i <= 42; i++) {
document.getElementById(this.id + '-day-num-' + i).innerHTML = "";
document.getElementById(this.id + '-day-' + i).className = this.id + " cjslib-day cjslib-day-listed";
}
var firstDay = new Date(this.date.getFullYear(), this.date.getMonth(), 1).getDay();
var lastDay = new Date(this.date.getMonth() + 1 > 11 ? this.date.getFullYear() + 1 : this.date.getFullYear(), this.date.getMonth() + 1 > 12 ? 0 : this.date.getMonth() + 1, 0).getDate();
var previousLastDay = new Date(this.date.getMonth() < 0 ? this.date.getFullYear() - 1 : this.date.getFullYear(), this.date.getMonth() < 0 ? 11 : this.date.getMonth(), 0).getDate();
this.initday = this.label.indexOf(this.defaultLabels[firstDay]);
var firstDayLabel = this.defaultLabels[firstDay];
var firstDayLabelPos = this.label.indexOf(firstDayLabel);
for (var i = 0, j = previousLastDay; i < firstDayLabelPos; i++, j--) {
document.getElementById(this.id + '-day-num-' + (firstDayLabelPos - i)).innerHTML = j;
document.getElementById(this.id + '-day-' + (firstDayLabelPos - i)).className = this.id + " cjslib-day cjslib-day-diluted";
}
for (var i = 1; i <= lastDay; i++) {
document.getElementById(this.id + '-day-num-' + (firstDayLabelPos + i)).innerHTML = i;
if (i == this.date.getDate()) document.getElementById(this.id + '-day-radio-' + (firstDayLabelPos + i)).checked = true;
if (this.date.getMonth() == this.today.getMonth())
if (i == this.today.getDate()) document.getElementById(this.id + '-day-' + (firstDayLabelPos + i)).className += " cjslib-day-today";
}
for (var i = lastDay + 1, j = 1; firstDayLabelPos + i <= 42; i++, j++) {
document.getElementById(this.id + '-day-num-' + (firstDayLabelPos + i)).innerHTML = j;
document.getElementById(this.id + '-day-' + (firstDayLabelPos + i)).className = this.id + " cjslib-day cjslib-day-diluted";
}
};
Calendar.prototype.setupBlock = function (blockId, calendarInstance, callback) {
document.getElementById(calendarInstance.id + "-day-" + blockId).onclick = function () {
if (document.getElementById(calendarInstance.id + "-day-num-" + blockId).innerHTML.length > 0) {
calendarInstance.changeDateTo(document.getElementById(calendarInstance.id + "-day-num-" + blockId).innerHTML, blockId);
callback();
}
};
};
Calendar.prototype.setOnClickListener = function (theCase, backCallback, nextCallback) {
var calendarId = this.id;
backCallback = backCallback || function () {};
nextCallback = nextCallback || function () {};
var calendarInstance = this;
switch (theCase) {
case "days-blocks":
for (var i = 1; i <= 42; i++) {
calendarInstance.setupBlock(i, calendarInstance, backCallback);
}
break;
case "month-slider":
document.getElementById(calendarId + "-month-back").onclick = function () {
calendarInstance.back('month');
backCallback();
};
document.getElementById(calendarId + "-month-next").onclick = function () {
calendarInstance.next('month');
nextCallback();
};
break;
case "year-slider":
document.getElementById(calendarId + "-year-back").onclick = function () {
calendarInstance.back('year');
backCallback();
};
document.getElementById(calendarId + "-year-next").onclick = function () {
calendarInstance.next('year');
nextCallback();
};
break;
}
};
function Organizer(id, calendar, data) {
this.id = id;
this.calendar = calendar;
this.data = data || {};
this.draw();
var organizerInstance = this;
organizerInstance.onMonthChange(function () {
organizerInstance.indicateEvents();
});
this.setOnClickListener('days-blocks');
this.setOnClickListener('day-slider');
this.setOnClickListener('month-slider');
this.setOnClickListener('year-slider');
this.setOnLongClickListener('days-blocks');
}
Organizer.prototype = {
constructor: Organizer,
back: function back(func) {
var date = this.calendar.date;
var lastDay = new Date(date.getMonth() + 1 > 11 ? date.getFullYear() + 1 : date.getFullYear(), date.getMonth() + 1 > 12 ? 0 : date.getMonth() + 1, 0).getDate();
var previousLastDay = new Date(date.getMonth() < 0 ? date.getFullYear() - 1 : date.getFullYear(), date.getMonth() < 0 ? 11 : date.getMonth(), 0).getDate();
if (func == "day") {
if (date.getDate() != 1) {
this.changeDateTo(date.getDate() - 1);
this.update();
} else {
this.calendar.back('month');
this.changeDateTo(lastDay);
var organizerInstance = this;
organizerInstance.onMonthChange(function () {
organizerInstance.indicateEvents();
});
}
document.getElementById(this.calendar.id + "-day-radio-" + (this.calendar.initday + date.getDate())).checked = true;
} else {
this.calendar.back(func);
var organizerInstance = this;
organizerInstance.onMonthChange(function () {
organizerInstance.indicateEvents();
});
}
},
next: function next(func) {
var date = this.calendar.date;
var lastDay = new Date(date.getMonth() + 1 > 11 ? date.getFullYear() + 1 : date.getFullYear(), date.getMonth() + 1 > 12 ? 0 : date.getMonth() + 1, 0).getDate();
var soonLastDay = new Date(date.getMonth() + 2 > 11 ? date.getFullYear() + 1 : date.getFullYear(), date.getMonth() + 2 > 12 ? 0 : date.getMonth() + 2, 0).getDate();
if (func == "day") {
if (date.getDate() != lastDay) {
date.setDate(date.getDate() + 1);
this.update();
} else {
this.calendar.next('month');
date.setDate(1);
var organizerInstance = this;
organizerInstance.onMonthChange(function () {
organizerInstance.indicateEvents();
});
}
document.getElementById(this.calendar.id + "-day-radio-" + (this.calendar.initday + date.getDate())).checked = true;
} else {
this.calendar.next(func);
var organizerInstance = this;
organizerInstance.onMonthChange(function () {
organizerInstance.indicateEvents();
});
}
},
changeDateTo: function changeDateTo(theDay, theBlock) {
this.clearHistory();
var changedMonth = this.calendar.changeDateTo(theDay, theBlock);
var organizerInstance = this;
setTimeout(function () {
if (changedMonth) {
organizerInstance.onMonthChange(function () {
organizerInstance.indicateEvents();
});
} else organizerInstance.update();
}, 1);
},
addDate: function changeDateTo(theDay, theBlock) {
this.showHistory();
var changedMonth = this.calendar.changeDateTo(theDay, theBlock);
var organizerInstance = this;
setTimeout(function () {
if (changedMonth) {
organizerInstance.onMonthChange(function () {
organizerInstance.indicateEvents();
});
} else organizerInstance.update();
}, 1);
}
};
Organizer.prototype.draw = function () {
var backSvg = '<svg style="width: 24px; height: 24px;" viewBox="0 0 24 24"><path fill="' + this.calendar.colors[3] + '" d="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z"></path></svg>';
var nextSvg = '<svg style="width: 24px; height: 24px;" viewBox="0 0 24 24"><path fill="' + this.calendar.colors[3] + '" d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z"></path></svg>';
var theOrganizer = document.createElement("DIV");
theOrganizer.className = "cjslib-events cjslib-size-" + this.calendar.size;
var theDate = document.createElement("DIV");
theDate.className = "cjslib-date";
theDate.style.backgroundColor = this.calendar.colors[1];
theDate.style.color = this.calendar.colors[3];
var backSlider = document.createElement("DIV");
backSlider.id = this.id + "-day-back";
backSlider.insertAdjacentHTML('beforeend', backSvg);
theDate.appendChild(backSlider.cloneNode(true));
var theText = document.createElement("SPAN");
theText.id = this.id + "-date";
theDate.appendChild(theText.cloneNode(true));
var nextSlider = document.createElement("DIV");
nextSlider.id = this.id + "-day-next";
nextSlider.insertAdjacentHTML('beforeend', nextSvg);
theDate.appendChild(nextSlider.cloneNode(true));
var theRows = document.createElement("DIV");
theRows.className = "cjslib-rows";
theRows.id = this.id + "-list-container";
var theList = document.createElement("OL");
theList.className = "cjslib-list";
theList.id = this.id + "-list";
var theHistory = document.createElement("OL");
theHistory.className = "cjslib-list";
theHistory.id = this.id + "-history";
theRows.appendChild(theList.cloneNode(true));
theRows.appendChild(theHistory.cloneNode(true));
theOrganizer.appendChild(theDate.cloneNode(true));
theOrganizer.appendChild(theRows.cloneNode(true));
document.getElementById(this.id).appendChild(theOrganizer.cloneNode(true));
};
Organizer.prototype.update = function () {
document.getElementById(this.id + "-date").innerHTML = this.calendar.months[this.calendar.date.getMonth()] + " " + this.calendar.date.getDate() + ", " + this.calendar.date.getFullYear();
document.getElementById(this.id + "-list").innerHTML = "";
this.showEvents();
};
Organizer.prototype.updateData = function (data) {
this.data = data;
this.indicateEvents();
this.update();
};
Organizer.prototype.list = function (data) {
var container = document.createElement("UL");
for (var i = 0; i < data.length; i++) {
var listItem = document.createElement("LI");
listItem.id = this.id + "-list-item-" + i;
var division = document.createElement("DIV");
var span = document.createElement("SPAN");
span.id = this.id + "-list-item-" + i + "-time";
span.class = this.id + " time";
span.appendChild(document.createTextNode(data[i].startTime + ' - ' + data[i].endTime));
division.appendChild(span);
var paragraph = document.createElement("P");
paragraph.id = this.id + "-list-item-" + i + "-text";
var textNode = document.createTextNode(data[i].text);
if (data[i].link == undefined || data[i].link == "") {
paragraph.appendChild(textNode);
} else {
var link = document.createElement("A");
link.href = data[i].link;
link.target = "_blank";
link.class = this.id + " link";
link.appendChild(textNode);
paragraph.appendChild(link);
}
listItem.appendChild(division);
listItem.appendChild(paragraph);
container.appendChild(listItem);
}
return container.innerHTML
};
Organizer.prototype.remember = function (date, content) {
if (content.startsWith("<div class=\"cjslib-list-placeholder\">"))
return "";
var dateTitle = this.calendar.getDateString();
this.calendar.history.unshift(dateTitle);
var container = document.createElement("UL");
container.className = "cjslib-list cjslib-list-history"
var title = document.createElement("LI");
title.appendChild(document.createTextNode(dateTitle));
title.className = "cjslib-list-history-title cjslib-date";
title.style.backgroundColor = this.calendar.colors[1];
title.style.color = this.calendar.colors[3];
container.appendChild(title);
container.innerHTML += content;
return container.outerHTML;
};
Organizer.prototype.clearHistory = function () {
this.calendar.history = [];
document.getElementById(this.id + "-history").innerHTML = "";
}
Organizer.prototype.setupBlock = function (blockId, organizerInstance, callback) {
var calendarInstance = organizerInstance.calendar;
document.getElementById(calendarInstance.id + "-day-" + blockId).onclick = function () {
if (document.getElementById(calendarInstance.id + "-day-num-" + blockId).innerHTML.length > 0) {
if (document.getElementById(calendarInstance.id + "-day-radio-" + blockId).checked)
return;
var longPressed = "" + document.getElementById(calendarInstance.id + "-day-num-" + blockId).dataset.longpressed;
document.getElementById(calendarInstance.id + "-day-num-" + blockId).dataset.longpressed = false;
if (longPressed != "true") {
organizerInstance.changeDateTo(document.getElementById(calendarInstance.id + "-day-num-" + blockId).innerHTML, blockId);
callback();
}
}
};
};
Organizer.prototype.showEvents = function (data) {
data = data || this.data;
var date = this.calendar.date;
var content = "";
var history = "";
try {
var historyIndex = this.calendar.history.indexOf(this.calendar.getDateString());
if (historyIndex > -1) {
this.calendar.history.splice(historyIndex, 1);
document.getElementById(this.id + "-history").children[historyIndex].remove();
}
history +=
history += document.getElementById(this.id + "-list").innerHTML;
content = this.list(data[date.getFullYear()][date.getMonth() + 1][date.getDate()]);
} catch (e) {
content = this.showPlaceholder();
}
document.getElementById(this.id + "-list").innerHTML = content;
};
Organizer.prototype.showHistory = function (data) {
data = data || this.data;
var date = this.calendar.date;
var content = this.remember(date, document.getElementById(this.id + "-list").innerHTML);
var history = document.getElementById(this.id + "-history").innerHTML;
document.getElementById(this.id + "-history").innerHTML = content + history;
};
Organizer.prototype.showPlaceholder = function (data) {
var container = document.createElement("DIV");
container.className = "cjslib-list-placeholder";
container.innerHTML = this.calendar.placeholder;
return container.outerHTML;
};
Organizer.prototype.indicateEvents = function (data) {
data = data || this.data;
var date = this.calendar.date;
if (this.calendar.indicator) {
var allDays = document.getElementsByClassName(this.calendar.id + " cjslib-day cjslib-day-listed");
for (var i = 0; i < allDays.length; i++) {
allDays[i].children[1].innerHTML = "";
}
// recolic customize
var allDaysInclCorners = document.getElementsByClassName(this.calendar.id + " cjslib-day");
for (var i = 0; i < allDaysInclCorners.length; i++) {
allDaysInclCorners[i].style.backgroundColor = "";
}
try {
var month = data[date.getFullYear()][date.getMonth() + 1];
for (var key in month) {
if (month[key].length > 0) {
if (this.calendar.indicator_type == 9) {
// recolic customize
allDays[key - 1].style.backgroundColor = "#dbcea7";
}
else {
allDays[key - 1].children[1].innerHTML = (month[key].length > 9) ? "9+" : month[key].length;
}
}
}
} catch (e) {}
}
this.update();
};
Organizer.prototype.onMonthChange = function (callback) {
callback();
};
Organizer.prototype.setOnClickListener = function (theCase, backCallback, nextCallback) {
var calendarId = this.calendar.id;
var organizerId = this.id;
backCallback = backCallback || function () {};
nextCallback = nextCallback || function () {};
var organizerInstance = this;
switch (theCase) {
case "days-blocks":
for (var i = 1; i <= 42; i++) {
organizerInstance.setupBlock(i, organizerInstance, backCallback);
}
break;
case "day-slider":
document.getElementById(organizerId + "-day-back").onclick = function () {
organizerInstance.back('day');
backCallback();
};
document.getElementById(organizerId + "-day-next").onclick = function () {
organizerInstance.next('day');
nextCallback();
};
break;
case "month-slider":
document.getElementById(calendarId + "-month-back").onclick = function () {
organizerInstance.back('month');
backCallback();
};
document.getElementById(calendarId + "-month-next").onclick = function () {
organizerInstance.next('month');
nextCallback();
};
break;
case "year-slider":
document.getElementById(calendarId + "-year-back").onclick = function () {
organizerInstance.back('year');
backCallback();
};
document.getElementById(calendarId + "-year-next").onclick = function () {
organizerInstance.next('year');
nextCallback();
};
break;
}
};
Organizer.prototype.setupLongClickBlock = function (blockId, organizerInstance, callback) {
var calendarInstance = organizerInstance.calendar;
var mouseDownEvent = function () {
document.getElementById(calendarInstance.id + "-day-num-" + blockId).dataset.longpressed = "-";
window.setTimeout(function () {
if (document.getElementById(calendarInstance.id + "-day-num-" + blockId).innerHTML.length > 0) {
if (document.getElementById(calendarInstance.id + "-day-num-" + blockId).dataset.longpressed == "false")
return;
else document.getElementById(calendarInstance.id + "-day-num-" + blockId).dataset.longpressed = true;
if (document.getElementById(calendarInstance.id + "-day-radio-" + blockId).checked)
return;
organizerInstance.addDate(document.getElementById(calendarInstance.id + "-day-num-" + blockId).innerHTML, blockId);
callback();
}
}, 1000);
};
document.getElementById(calendarInstance.id + "-day-" + blockId).onmousedown = mouseDownEvent;
document.getElementById(calendarInstance.id + "-day-" + blockId).ontouchstart = mouseDownEvent;
};
Organizer.prototype.setOnLongClickListener = function (theCase, backCallback, nextCallback) {
var calendarId = this.calendar.id;
var organizerId = this.id;
backCallback = backCallback || function () {};
nextCallback = nextCallback || function () {};
var organizerInstance = this;
switch (theCase) {
case "days-blocks":
for (var i = 1; i <= 42; i++) {
organizerInstance.setupLongClickBlock(i, organizerInstance, backCallback);
}
break;
}
}
<!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