Compare commits
5 Commits
544d6962e2
...
46b4820bce
Author | SHA1 | Date |
---|---|---|
Anantha Kumaran | 46b4820bce | |
Anantha Kumaran | 4bcf2cce8c | |
Anantha Kumaran | 8942670f22 | |
Anantha Kumaran | 78fe6dcacf | |
Anantha Kumaran | 3b985c9716 |
|
@ -6,6 +6,7 @@ categories:
|
|||
- locale
|
||||
hide:
|
||||
- feedback
|
||||
description: "How to setup Paisa for your country."
|
||||
---
|
||||
|
||||
# Localization
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
---
|
||||
date: 2024-01-26
|
||||
comments: true
|
||||
categories:
|
||||
- sovereign-gold-bond
|
||||
- gold
|
||||
- sgb
|
||||
- price
|
||||
hide:
|
||||
- feedback
|
||||
description: "How to track Sovereign Gold Bond price and calculate returns."
|
||||
---
|
||||
|
||||
# Sovereign Gold Bond
|
||||
|
||||
Reserve Bank of India started issuing a new type of bond called
|
||||
[Sovereign Gold Bond](https://www.rbi.org.in/commonperson/English/Scripts/FAQs.aspx?Id=1658) (SGB) in November 2015. The bond is issued by
|
||||
RBI on behalf of the Government of India. Each unit of the bond is
|
||||
equivalent to a gram of gold. The price of the bond is linked to the
|
||||
price of the gold. The price is calculated by taking the average of
|
||||
the closing price of gold of 999 purity for the last 3 business days
|
||||
of the week. RBI follows the price of gold published by [IBJA](https://ibja.co/).
|
||||
|
||||
<!-- more -->
|
||||
|
||||
The bond also includes an interest component. The interest is paid
|
||||
every 6 months. The initial bonds were issued with 2.75%
|
||||
interest. Nowadays the interest is 2.5%. The interest is taxable as
|
||||
per the income tax slab of the investor. Any capital gains on the bond
|
||||
are exempted if you hold the bond till maturity. The bond has an 8
|
||||
year term with an exit option available from the 5<sup>th</sup> year
|
||||
onward. In this blog post, we'll explore how to use Paisa to
|
||||
effectively track your SGBs and calculate your returns.
|
||||
|
||||
Let's assume you have `#!ledger 25,000 INR` in your bank account.
|
||||
|
||||
```ledger
|
||||
2015/11/01 Opening Balance
|
||||
Assets:Checking:SBI 25000.00 INR
|
||||
Equity:OpeningBalance
|
||||
```
|
||||
|
||||
Let's assume you bought 8 units of SGB issued on 26<sup>th</sup>
|
||||
November 2015. The price of a single unit was `#!ledger 2684 INR`.
|
||||
|
||||
```ledger
|
||||
2015/11/26 Buy Sovereign Gold Bond
|
||||
Assets:Gold:SGB 8 SGB @ 2684.00 INR
|
||||
Assets:Checking:SBI -21472.00 INR
|
||||
```
|
||||
|
||||
If you go to the `#!ledger Assets:Gold:SGB` account, you'll see the
|
||||
following
|
||||
|
||||
![Initial Balance](../../images/sgb/balance-1.png)
|
||||
|
||||
Let's add the interest component to the bond. The interest is paid on
|
||||
26<sup>th</sup> of May and November every year. We need to make two
|
||||
transactions entries for each interest payment. One from `#!ledger
|
||||
Income:Interest:Gold:SGB` to `#!ledger Assets:Gold:SGB` and another
|
||||
from `#!ledger Assets:Gold:SGB` to `#!ledger
|
||||
Assets:Checking:SBI`. This is necessary to calculate the correct
|
||||
returns. If you send the interest directly to your bank account, Paisa
|
||||
will not be able to calculate the correct returns for `#!ledger
|
||||
Income:Interest:Gold:SGB` account.
|
||||
|
||||
```ledger
|
||||
2016/05/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2016/05/26 SGB Interest Credit
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2016/11/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2016/11/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2017/05/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2017/05/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2017/11/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2017/11/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2018/05/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2018/05/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2018/11/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2018/11/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2019/05/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2019/05/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2019/11/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2019/11/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2020/05/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2020/05/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2020/11/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2020/11/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2021/05/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2021/05/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2021/11/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2021/11/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2022/05/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2022/05/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2022/11/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2022/11/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2023/05/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2023/05/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
|
||||
2023/11/26 SGB Interest Credit
|
||||
Assets:Gold:SGB 295.24 INR
|
||||
Income:Interest:Gold:SGB
|
||||
|
||||
2023/11/26 SGB Interest Withdrawal
|
||||
Assets:Gold:SGB -295.24 INR
|
||||
Assets:Checking:SBI
|
||||
```
|
||||
|
||||
The final part is the bond settlement. The debit posting includes the
|
||||
buy price, buy date and the sell price. The gains come from the
|
||||
capital gains account.
|
||||
|
||||
```ledger
|
||||
2023/11/30 Withdraw Sovereign Gold Bond
|
||||
Assets:Gold:SGB -8 SGB {2684.00 INR} [2015/11/26] @ 6132.00 INR
|
||||
Income:CapitalGains:Gold:SGB -27584 INR
|
||||
Assets:Checking:SBI 49056.00 INR
|
||||
```
|
||||
|
||||
It's also possible to track the daily price[^1] of the bond. Since the
|
||||
bond price is linked to the price of gold, we can use the IBJA gold
|
||||
price to track the price of the bond. Go to the configuration page and
|
||||
add a new commodity with the following information.
|
||||
|
||||
![Commodity Settings](../../images/sgb/config.png)
|
||||
|
||||
If you go to the `#!ledger Assets:Gold:SGB` account, You can find the
|
||||
total returns, XIRR and the change of the bond value over time.
|
||||
|
||||
![Final Balance](../../images/sgb/balance-2.png)
|
||||
|
||||
|
||||
[^1]: The daily price history of IBJA gold price is available only
|
||||
from 2022. If you have access to the price history, you can ping
|
||||
me. Alternatively, you can manually add the [price history](../../reference/commodities.md#realestate) to the
|
||||
ledger file.
|
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
After Width: | Height: | Size: 187 KiB |
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -269,4 +269,20 @@ user_accounts:
|
|||
password: sha256:a96dc73edd639b1c711b006e714bd2ff5bf5c1aecd77d0b3c3370403c66d58e5
|
||||
# Required, password hashed twice with sha256, then prefixed sha256:
|
||||
# echo -n 'secret' | sha256sum | head -c 64 | sha256sum | head -c 64
|
||||
|
||||
## List of credit cards
|
||||
# OPTIONAL, DEFAULT: []
|
||||
credit_cards:
|
||||
- account: Liabilities:CreditCard:Freedom
|
||||
# Required, account name
|
||||
credit_limit: 150000
|
||||
# Required, credit limit of the card
|
||||
statement_end_day: 8
|
||||
# Required, the day of the month when the statement is generated
|
||||
due_day: 20
|
||||
# Required, the day of the month when the payment is due
|
||||
network: visa
|
||||
# Required, the network of the card
|
||||
number: "0007"
|
||||
# Required, the last 4 digits of the card number
|
||||
```
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
description: "How to configure credit cards in Paisa"
|
||||
---
|
||||
|
||||
# Credit Cards
|
||||
|
||||
Paisa allows you to track your credit card bills and payments. Let's
|
||||
say you have a liability account called
|
||||
`Liabilities:CreditCard:Freedom` to track your real world credit card,
|
||||
you can configure it in Paisa as follows:
|
||||
|
||||
```yaml
|
||||
credit_cards:
|
||||
- account: Liabilities:CreditCard:Freedom #(1)!
|
||||
credit_limit: 150000 #(2)!
|
||||
statement_end_day: 8 #(3)!
|
||||
due_day: 20 #(4)!
|
||||
network: visa #(5)!
|
||||
number: "0007" #(6)!
|
||||
```
|
||||
|
||||
1. Account name
|
||||
2. Credit limit of the card
|
||||
3. The day of the month when the statement is generated
|
||||
4. The day of the month when the payment is due
|
||||
5. The network of the card
|
||||
6. The last 4 digits of the card number
|
||||
|
||||
The above configuration can be done from the `More > Configuration`
|
||||
page. Expand the `Credit Cards` section and click
|
||||
:fontawesome-solid-circle-plus: icon to add a new one
|
||||
|
||||
Once configured, the credit card will show up on the `Liabilities >
|
||||
Credit Cards` page. Paisa will automatically calculate and display
|
||||
various information like the amount due, payment due date, credit
|
||||
limit utilized, etc.
|
|
@ -323,3 +323,7 @@
|
|||
background-color: var(--account-color-income-background);
|
||||
color: var(--account-color-income);
|
||||
}
|
||||
|
||||
.language-ledger pre code {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||
"github.com/ananthakumaran/paisa/internal/model/transaction"
|
||||
"github.com/ananthakumaran/paisa/internal/service"
|
||||
"github.com/ananthakumaran/paisa/internal/utils"
|
||||
"github.com/samber/lo"
|
||||
|
@ -186,6 +187,11 @@ func RunningBalance(db *gorm.DB, postings []posting.Posting) []Point {
|
|||
return series
|
||||
}
|
||||
|
||||
func SortTransactionAsc(transactions []transaction.Transaction) []transaction.Transaction {
|
||||
sort.Slice(transactions, func(i, j int) bool { return transactions[i].Date.Before(transactions[j].Date) })
|
||||
return transactions
|
||||
}
|
||||
|
||||
func SortAsc(postings []posting.Posting) []posting.Posting {
|
||||
sort.Slice(postings, func(i, j int) bool { return postings[i].Date.Before(postings[j].Date) })
|
||||
return postings
|
||||
|
@ -212,3 +218,13 @@ func GroupByAccount(posts []posting.Posting) map[string][]posting.Posting {
|
|||
return post.Account
|
||||
})
|
||||
}
|
||||
|
||||
func GroupByMonthlyBillingCycle(postsings []posting.Posting, billDate int) map[string][]posting.Posting {
|
||||
return lo.GroupBy(postsings, func(p posting.Posting) string {
|
||||
if p.Date.Day() > billDate {
|
||||
return utils.BeginningOfMonth(p.Date).AddDate(0, 1, 0).Format("2006-01")
|
||||
} else {
|
||||
return p.Date.Format("2006-01")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package cache
|
|||
|
||||
import (
|
||||
"github.com/ananthakumaran/paisa/internal/accounting"
|
||||
"github.com/ananthakumaran/paisa/internal/model/transaction"
|
||||
"github.com/ananthakumaran/paisa/internal/prediction"
|
||||
"github.com/ananthakumaran/paisa/internal/service"
|
||||
)
|
||||
|
@ -11,4 +12,5 @@ func Clear() {
|
|||
service.ClearPriceCache()
|
||||
accounting.ClearCache()
|
||||
prediction.ClearCache()
|
||||
transaction.ClearCache()
|
||||
}
|
||||
|
|
|
@ -115,6 +115,15 @@ type AllocationTarget struct {
|
|||
Accounts []string `json:"accounts" yaml:"accounts"`
|
||||
}
|
||||
|
||||
type CreditCard struct {
|
||||
Account string `json:"account" yaml:"account"`
|
||||
CreditLimit int `json:"credit_limit" yaml:"credit_limit"`
|
||||
StatementEndDay int `json:"statement_end_day" yaml:"statement_end_day"`
|
||||
DueDay int `json:"due_day" yaml:"due_day"`
|
||||
Network string `json:"network" yaml:"network"`
|
||||
Number string `json:"number" yaml:"number"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
JournalPath string `json:"journal_path" yaml:"journal_path"`
|
||||
DBPath string `json:"db_path" yaml:"db_path"`
|
||||
|
@ -143,6 +152,8 @@ type Config struct {
|
|||
Goals Goals `json:"goals" yaml:"goals"`
|
||||
|
||||
UserAccounts []UserAccount `json:"user_accounts" yaml:"user_accounts"`
|
||||
|
||||
CreditCards []CreditCard `json:"credit_cards" yaml:"credit_cards"`
|
||||
}
|
||||
|
||||
var config Config
|
||||
|
@ -166,6 +177,7 @@ var defaultConfig = Config{
|
|||
Accounts: []Account{},
|
||||
Goals: Goals{Retirement: []RetirementGoal{}, Savings: []SavingsGoal{}},
|
||||
UserAccounts: []UserAccount{},
|
||||
CreditCards: []CreditCard{},
|
||||
}
|
||||
|
||||
var itemsUniquePropertiesMeta = jsonschema.MustCompileString("itemsUniqueProperties.json", `{
|
||||
|
|
|
@ -347,7 +347,7 @@
|
|||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "name of the commodity"
|
||||
"description": "Name of the commodity"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
|
@ -401,7 +401,7 @@
|
|||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "name of the template",
|
||||
"description": "Name of the template",
|
||||
"minLength": 1
|
||||
},
|
||||
"content": {
|
||||
|
@ -428,7 +428,7 @@
|
|||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "name of the account",
|
||||
"description": "Name of the account",
|
||||
"minLength": 1
|
||||
},
|
||||
"icon": {
|
||||
|
@ -440,6 +440,66 @@
|
|||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"credit_cards": {
|
||||
"type": "array",
|
||||
"itemsUniqueProperties": ["account"],
|
||||
"default": [
|
||||
{
|
||||
"account": "Liabilities:CreditCard:Chase",
|
||||
"credit_limit": 100000,
|
||||
"statement_end_day": 28,
|
||||
"due_day": 15
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"ui:header": "account",
|
||||
"properties": {
|
||||
"account": {
|
||||
"type": "string",
|
||||
"description": "Name of the credit card account"
|
||||
},
|
||||
"credit_limit": {
|
||||
"type": "number",
|
||||
"description": "Credit limit of the card",
|
||||
"minimum": 1
|
||||
},
|
||||
"statement_end_day": {
|
||||
"type": "integer",
|
||||
"description": "Statement end day of the card",
|
||||
"minimum": 1,
|
||||
"maximum": 31
|
||||
},
|
||||
"due_day": {
|
||||
"type": "integer",
|
||||
"description": "Due day of the card",
|
||||
"minimum": 1,
|
||||
"maximum": 31
|
||||
},
|
||||
"network": {
|
||||
"type": "string",
|
||||
"description": "Network of the card",
|
||||
"enum": ["visa", "mastercard", "dinersclub", "amex", "rupay", "jcb", "discover"]
|
||||
},
|
||||
"number": {
|
||||
"type": "string",
|
||||
"description": "Last 4 digits of the card number",
|
||||
"maxLength": 4,
|
||||
"minLength": 4,
|
||||
"pattern": "^[0-9]{4}$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"account",
|
||||
"credit_limit",
|
||||
"statement_end_day",
|
||||
"due_day",
|
||||
"network",
|
||||
"number"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["journal_path", "db_path"],
|
||||
|
|
|
@ -25,13 +25,14 @@ import (
|
|||
const START_YEAR = 2014
|
||||
|
||||
type GeneratorState struct {
|
||||
Balance float64
|
||||
EPFBalance float64
|
||||
Ledger *os.File
|
||||
YearlySalary float64
|
||||
Rent float64
|
||||
LoanBalance float64
|
||||
NiftyBalance float64
|
||||
Balance float64
|
||||
CreditBalance float64
|
||||
EPFBalance float64
|
||||
Ledger *os.File
|
||||
YearlySalary float64
|
||||
Rent float64
|
||||
LoanBalance float64
|
||||
NiftyBalance float64
|
||||
}
|
||||
|
||||
var pricesTree map[string]*btree.BTree
|
||||
|
@ -102,6 +103,9 @@ allocation_targets:
|
|||
target: 60
|
||||
accounts:
|
||||
- Assets:Equity:*
|
||||
accounts:
|
||||
- name: Liabilities:CreditCard:Freedom
|
||||
icon: arcticons:chase
|
||||
schedule_al:
|
||||
- code: bank
|
||||
accounts:
|
||||
|
@ -153,6 +157,13 @@ commodities:
|
|||
price:
|
||||
provider: com-purifiedbytes-nps
|
||||
code: SM008003
|
||||
credit_cards:
|
||||
- account: Liabilities:CreditCard:Freedom
|
||||
credit_limit: 150000
|
||||
statement_end_day: 8
|
||||
due_day: 20
|
||||
network: visa
|
||||
number: "0007"
|
||||
`
|
||||
log.Info("Generating config file: ", configFilePath)
|
||||
journalFilePath := filepath.Join(cwd, "main.ledger")
|
||||
|
@ -326,18 +337,30 @@ func emitExpense(state *GeneratorState, start time.Time) {
|
|||
}
|
||||
|
||||
emit := func(payee string, account string, amount float64, fuzz float64) {
|
||||
actualAmount := roundToK(percentRange(int(fuzz*100), 100) * amount)
|
||||
var actualAmount float64
|
||||
if fuzz == 1 {
|
||||
actualAmount = amount
|
||||
} else {
|
||||
actualAmount = roundToK(percentRange(int(fuzz*100), 100) * amount)
|
||||
}
|
||||
start = start.AddDate(0, 0, 1)
|
||||
emitTransaction(state.Ledger, start, payee, "Assets:Checking:SBI", account, actualAmount)
|
||||
state.Balance -= actualAmount
|
||||
}
|
||||
|
||||
emit("Rent", "Expenses:Rent", state.Rent, 1.0)
|
||||
emit("Internet", "Expenses:Utilities", 1500, 1.0)
|
||||
emit("Mobile", "Expenses:Utilities", 430, 1.0)
|
||||
emit("Shopping", "Expenses:Shopping", 3000, 0.5)
|
||||
emit("Eat out", "Expenses:Restaurants", 2500, 0.5)
|
||||
emit("Groceries", "Expenses:Food", 5000, 0.9)
|
||||
emitExpense := func(payee string, account string, amount float64, fuzz float64) {
|
||||
actualAmount := roundToK(percentRange(int(fuzz*100), 100) * amount)
|
||||
start = start.AddDate(0, 0, 1)
|
||||
emitTransaction(state.Ledger, start, payee, "Liabilities:CreditCard:Freedom", account, actualAmount)
|
||||
state.CreditBalance -= actualAmount
|
||||
}
|
||||
|
||||
emitExpense("Rent", "Expenses:Rent", state.Rent, 1.0)
|
||||
emitExpense("Internet", "Expenses:Utilities", 1500, 1.0)
|
||||
emitExpense("Mobile", "Expenses:Utilities", 430, 1.0)
|
||||
emitExpense("Shopping", "Expenses:Shopping", 3000, 0.5)
|
||||
emitExpense("Eat out", "Expenses:Restaurants", 2500, 0.5)
|
||||
emitExpense("Groceries", "Expenses:Food", 5000, 0.9)
|
||||
|
||||
if state.LoanBalance > 0 {
|
||||
emi := math.Min(state.Balance-10000, 30000.0)
|
||||
|
@ -349,12 +372,17 @@ func emitExpense(state *GeneratorState, start time.Time) {
|
|||
}
|
||||
|
||||
if state.Balance < 10000 {
|
||||
emit("Pay Credit Card Bill", "Liabilities:CreditCard:Freedom", -state.CreditBalance, 1.0)
|
||||
state.CreditBalance = 0
|
||||
return
|
||||
}
|
||||
|
||||
if lo.Contains([]time.Month{time.January, time.April, time.November, time.December}, start.Month()) {
|
||||
emit("Dress", "Expenses:Clothing", 5000, 0.5)
|
||||
}
|
||||
|
||||
emit("Pay Credit Card Bill", "Liabilities:CreditCard:Freedom", -state.CreditBalance, 1.0)
|
||||
state.CreditBalance = 0
|
||||
}
|
||||
func emitInvestment(state *GeneratorState, start time.Time) {
|
||||
if start.Month() == time.April {
|
||||
|
@ -385,7 +413,6 @@ func emitInvestment(state *GeneratorState, start time.Time) {
|
|||
state.NiftyBalance += units
|
||||
state.Balance += amount
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func generateJournalFile(cwd string) {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package transaction
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||
"github.com/ananthakumaran/paisa/internal/query"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Transaction struct {
|
||||
|
@ -20,6 +23,32 @@ type Transaction struct {
|
|||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
type transactionCache struct {
|
||||
sync.Once
|
||||
transactions map[string]Transaction
|
||||
}
|
||||
|
||||
var tcache transactionCache
|
||||
|
||||
func loadTransactionCache(db *gorm.DB) {
|
||||
postings := query.Init(db).All()
|
||||
tcache.transactions = make(map[string]Transaction)
|
||||
|
||||
for _, t := range Build(postings) {
|
||||
tcache.transactions[t.ID] = t
|
||||
}
|
||||
}
|
||||
|
||||
func GetById(db *gorm.DB, id string) (Transaction, bool) {
|
||||
tcache.Do(func() { loadTransactionCache(db) })
|
||||
t, found := tcache.transactions[id]
|
||||
return t, found
|
||||
}
|
||||
|
||||
func ClearCache() {
|
||||
tcache = transactionCache{}
|
||||
}
|
||||
|
||||
func Build(postings []posting.Posting) []Transaction {
|
||||
grouped := lo.GroupBy(postings, func(p posting.Posting) string { return p.TransactionID })
|
||||
return lo.Map(lo.Values(grouped), func(ps []posting.Posting, _ int) Transaction {
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ananthakumaran/paisa/internal/accounting"
|
||||
"github.com/ananthakumaran/paisa/internal/config"
|
||||
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||
"github.com/ananthakumaran/paisa/internal/model/transaction"
|
||||
"github.com/ananthakumaran/paisa/internal/query"
|
||||
"github.com/ananthakumaran/paisa/internal/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/shopspring/decimal"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CreditCardSummary struct {
|
||||
Account string `json:"account"`
|
||||
Network string `json:"network"`
|
||||
Number string `json:"number"`
|
||||
Balance decimal.Decimal `json:"balance"`
|
||||
Bills []CreditCardBill `json:"bills"`
|
||||
CreditLimit decimal.Decimal `json:"creditLimit"`
|
||||
}
|
||||
|
||||
type CreditCardBill struct {
|
||||
StatementStartDate time.Time `json:"statementStartDate"`
|
||||
StatementEndDate time.Time `json:"statementEndDate"`
|
||||
DueDate time.Time `json:"dueDate"`
|
||||
PaidDate *time.Time `json:"paidDate"`
|
||||
Credits decimal.Decimal `json:"credits"`
|
||||
Debits decimal.Decimal `json:"debits"`
|
||||
DebitsRunningBalance decimal.Decimal
|
||||
OpeningBalance decimal.Decimal `json:"openingBalance"`
|
||||
ClosingBalance decimal.Decimal `json:"closingBalance"`
|
||||
Postings []posting.Posting `json:"postings"`
|
||||
Transactions []transaction.Transaction `json:"transactions"`
|
||||
}
|
||||
|
||||
func GetCreditCards(db *gorm.DB) gin.H {
|
||||
creditCards := []CreditCardSummary{}
|
||||
|
||||
for _, creditCardConfig := range config.GetConfig().CreditCards {
|
||||
ps := query.Init(db).Where("account = ?", creditCardConfig.Account).All()
|
||||
creditCards = append(creditCards, buildCreditCard(db, creditCardConfig, ps, false))
|
||||
}
|
||||
|
||||
return gin.H{"creditCards": creditCards}
|
||||
}
|
||||
|
||||
func GetCreditCard(db *gorm.DB, account string) gin.H {
|
||||
for _, creditCardConfig := range config.GetConfig().CreditCards {
|
||||
if creditCardConfig.Account == account {
|
||||
ps := query.Init(db).Where("account = ?", creditCardConfig.Account).All()
|
||||
creditCard := buildCreditCard(db, creditCardConfig, ps, true)
|
||||
return gin.H{"creditCard": creditCard, "found": true}
|
||||
}
|
||||
}
|
||||
|
||||
return gin.H{"found": false}
|
||||
}
|
||||
|
||||
func buildCreditCard(db *gorm.DB, creditCardConfig config.CreditCard, ps []posting.Posting, includePostings bool) CreditCardSummary {
|
||||
bills := computeBills(db, creditCardConfig, ps, includePostings)
|
||||
balance := decimal.Zero
|
||||
if len(bills) > 0 {
|
||||
balance = bills[len(bills)-1].ClosingBalance
|
||||
}
|
||||
return CreditCardSummary{
|
||||
Account: creditCardConfig.Account,
|
||||
Network: creditCardConfig.Network,
|
||||
Number: creditCardConfig.Number,
|
||||
Balance: balance,
|
||||
Bills: bills,
|
||||
CreditLimit: decimal.NewFromInt(int64(creditCardConfig.CreditLimit)),
|
||||
}
|
||||
}
|
||||
|
||||
func computeBills(db *gorm.DB, creditCardConfig config.CreditCard, ps []posting.Posting, includePostings bool) []CreditCardBill {
|
||||
bills := []CreditCardBill{}
|
||||
|
||||
grouped := accounting.GroupByMonthlyBillingCycle(ps, creditCardConfig.StatementEndDay)
|
||||
|
||||
balance := decimal.Zero
|
||||
creditsRunningBalance := decimal.Zero
|
||||
debitsRunningBalance := decimal.Zero
|
||||
unpaidBill := 0
|
||||
|
||||
for _, month := range utils.SortedKeys(grouped) {
|
||||
statementEndDate, err := time.Parse("2006-01", month)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
statementEndDate = statementEndDate.AddDate(0, 0, creditCardConfig.StatementEndDay-1)
|
||||
statementStartDate := statementEndDate.AddDate(0, -1, 1)
|
||||
|
||||
var dueDate time.Time
|
||||
if creditCardConfig.StatementEndDay < creditCardConfig.DueDay {
|
||||
dueDate = utils.BeginningOfMonth(statementEndDate).AddDate(0, 0, creditCardConfig.DueDay-1)
|
||||
} else {
|
||||
dueDate = utils.BeginningOfMonth(statementEndDate).AddDate(0, 1, creditCardConfig.DueDay-1)
|
||||
}
|
||||
|
||||
bill := CreditCardBill{
|
||||
StatementStartDate: statementStartDate,
|
||||
StatementEndDate: statementEndDate,
|
||||
DueDate: dueDate,
|
||||
OpeningBalance: balance,
|
||||
Postings: []posting.Posting{},
|
||||
Transactions: []transaction.Transaction{},
|
||||
}
|
||||
|
||||
transactionIDs := map[string]bool{}
|
||||
|
||||
for _, p := range grouped[month] {
|
||||
balance = balance.Add(p.Amount.Neg())
|
||||
|
||||
if p.Amount.IsPositive() {
|
||||
creditsRunningBalance = creditsRunningBalance.Add(p.Amount)
|
||||
bill.Credits = bill.Credits.Add(p.Amount)
|
||||
for unpaidBill < len(bills) {
|
||||
if bills[unpaidBill].DebitsRunningBalance.LessThanOrEqual(creditsRunningBalance) {
|
||||
paidDate := p.Date
|
||||
bills[unpaidBill].PaidDate = &paidDate
|
||||
unpaidBill++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bill.Debits = bill.Debits.Add(p.Amount.Neg())
|
||||
debitsRunningBalance = debitsRunningBalance.Add(p.Amount.Neg())
|
||||
}
|
||||
|
||||
if includePostings {
|
||||
bill.Postings = append(bill.Postings, p)
|
||||
transactionIDs[p.TransactionID] = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bill.DebitsRunningBalance = debitsRunningBalance
|
||||
bill.ClosingBalance = balance
|
||||
bill.Transactions = lo.Map(lo.Keys(transactionIDs), func(id string, _ int) transaction.Transaction {
|
||||
t, _ := transaction.GetById(db, id)
|
||||
return t
|
||||
})
|
||||
accounting.SortTransactionAsc(bill.Transactions)
|
||||
bills = append(bills, bill)
|
||||
}
|
||||
|
||||
return bills
|
||||
}
|
|
@ -363,6 +363,14 @@ func Build(db *gorm.DB, enableCompression bool) *gin.Engine {
|
|||
c.JSON(200, goal.GetGoalDetails(db, c.Param("type"), c.Param("name")))
|
||||
})
|
||||
|
||||
router.GET("/api/credit_cards", func(c *gin.Context) {
|
||||
c.JSON(200, GetCreditCards(db))
|
||||
})
|
||||
|
||||
router.GET("/api/credit_cards/:account", func(c *gin.Context) {
|
||||
c.JSON(200, GetCreditCard(db, c.Param("account")))
|
||||
})
|
||||
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(web.Index))
|
||||
})
|
||||
|
|
|
@ -12,22 +12,6 @@ import (
|
|||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type transactionCache struct {
|
||||
sync.Once
|
||||
transactions map[string]transaction.Transaction
|
||||
}
|
||||
|
||||
var tcache transactionCache
|
||||
|
||||
func loadTransactionCache(db *gorm.DB) {
|
||||
postings := query.Init(db).All()
|
||||
tcache.transactions = make(map[string]transaction.Transaction)
|
||||
|
||||
for _, t := range transaction.Build(postings) {
|
||||
tcache.transactions[t.ID] = t
|
||||
}
|
||||
}
|
||||
|
||||
type interestCache struct {
|
||||
sync.Once
|
||||
postings map[int64][]posting.Posting
|
||||
|
@ -76,13 +60,11 @@ func IsCapitalGains(p posting.Posting) bool {
|
|||
}
|
||||
|
||||
func IsStockSplit(db *gorm.DB, p posting.Posting) bool {
|
||||
tcache.Do(func() { loadTransactionCache(db) })
|
||||
|
||||
if utils.IsCurrency(p.Commodity) {
|
||||
return false
|
||||
}
|
||||
|
||||
t, found := tcache.transactions[p.TransactionID]
|
||||
t, found := transaction.GetById(db, p.TransactionID)
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
|
@ -96,13 +78,11 @@ func IsStockSplit(db *gorm.DB, p posting.Posting) bool {
|
|||
}
|
||||
|
||||
func IsSellWithCapitalGains(db *gorm.DB, p posting.Posting) bool {
|
||||
tcache.Do(func() { loadTransactionCache(db) })
|
||||
|
||||
if utils.IsCurrency(p.Commodity) {
|
||||
return false
|
||||
}
|
||||
|
||||
t, found := tcache.transactions[p.TransactionID]
|
||||
t, found := transaction.GetById(db, p.TransactionID)
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -13,17 +13,18 @@ func XIRR(db *gorm.DB, ps []posting.Posting) decimal.Decimal {
|
|||
today := utils.EndOfToday()
|
||||
marketAmount := utils.SumBy(ps, func(p posting.Posting) decimal.Decimal {
|
||||
if IsCapitalGains(p) {
|
||||
return p.Amount.Neg()
|
||||
return decimal.Zero
|
||||
}
|
||||
return p.MarketAmount
|
||||
})
|
||||
cashflows := lo.Reverse(lo.Map(ps, func(p posting.Posting, _ int) xirr.Cashflow {
|
||||
if IsInterest(db, p) || IsInterestRepayment(db, p) || IsCapitalGains(p) {
|
||||
if IsInterest(db, p) || IsInterestRepayment(db, p) {
|
||||
return xirr.Cashflow{Date: p.Date, Amount: 0}
|
||||
} else {
|
||||
return xirr.Cashflow{Date: p.Date, Amount: p.Amount.Neg().Round(4).InexactFloat64()}
|
||||
}
|
||||
}))
|
||||
|
||||
cashflows = append(cashflows, xirr.Cashflow{Date: today, Amount: marketAmount.Round(4).InexactFloat64()})
|
||||
return xirr.XIRR(cashflows)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ nav:
|
|||
- reference/ledger-cli.md
|
||||
- reference/editor.md
|
||||
- reference/user-authentication.md
|
||||
- reference/credit-cards.md
|
||||
- reference/analysis.md
|
||||
- 'Tax':
|
||||
- reference/tax/index.md
|
||||
|
|
33
src/app.scss
33
src/app.scss
|
@ -1088,3 +1088,36 @@ textarea:invalid {
|
|||
div.is-hoverable:hover {
|
||||
background-color: $white-bis;
|
||||
}
|
||||
|
||||
// credit card
|
||||
|
||||
.credit-card-container {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(19rem, 25rem));
|
||||
}
|
||||
|
||||
.credit-card {
|
||||
aspect-ratio: 3.375/2.125;
|
||||
max-width: 25rem;
|
||||
min-width: 19rem;
|
||||
flex: 1;
|
||||
border-radius: 0.7rem;
|
||||
display: flex;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3) !important;
|
||||
background: linear-gradient(
|
||||
345deg,
|
||||
$grey-lightest 0%,
|
||||
$grey-lightest 60%,
|
||||
$grey-lighter 60%,
|
||||
$grey-lighter 85%,
|
||||
$grey-light 85%,
|
||||
$grey-light 95%,
|
||||
$grey 95%,
|
||||
$grey 100%
|
||||
);
|
||||
|
||||
.chip {
|
||||
color: $amber-700;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -179,4 +179,19 @@ html[data-theme="dark"] {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.credit-card {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 1) !important;
|
||||
background: linear-gradient(
|
||||
345deg,
|
||||
$white 0%,
|
||||
$white 60%,
|
||||
$white-bis 60%,
|
||||
$white-bis 85%,
|
||||
$white-ter 85%,
|
||||
$white-ter 95%,
|
||||
$grey-lightest 95%,
|
||||
$grey-lightest 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -313,6 +313,7 @@ const COLORS = {
|
|||
primary: MaterialUI.deeppurple.a100,
|
||||
secondary: MaterialUI.lightblue.a400,
|
||||
tertiary: MaterialUI.amber.a400,
|
||||
neutral: "hsl(0, 0%, 48%)",
|
||||
assets: MaterialUI.lightblue.a400,
|
||||
expenses: MaterialUI.red.a400,
|
||||
income: MaterialUI.lime.a700,
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts">
|
||||
import { iconText } from "$lib/icon";
|
||||
import {
|
||||
formatCurrency,
|
||||
formatPercentage,
|
||||
restName,
|
||||
type CreditCardSummary,
|
||||
now,
|
||||
type CreditCardBill
|
||||
} from "$lib/utils";
|
||||
import _ from "lodash";
|
||||
import CreditCardNetwork from "./CreditCardNetwork.svelte";
|
||||
import DueDate from "./DueDate.svelte";
|
||||
|
||||
export let creditCard: CreditCardSummary;
|
||||
|
||||
function lastBill(creditCard: CreditCardSummary): CreditCardBill {
|
||||
return _.find(_.reverse(_.clone(creditCard.bills)), (b) => {
|
||||
return b.statementEndDate.isSameOrBefore(now());
|
||||
});
|
||||
}
|
||||
|
||||
$: bill = lastBill(creditCard);
|
||||
</script>
|
||||
|
||||
<div class="credit-card box p-3 m-0 flex-col justify-between">
|
||||
<div class="is-flex justify-between has-text-weight-bold is-size-5">
|
||||
<div style="margin: 35px 0 0 15px;" class="opacity-20 chip">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M10 4h10c1.11 0 2 .89 2 2v2h-3.41L16 10.59v4l-2 2V20h-4v-3.41l-2-2V9.41l2-2zm8 7.41V14h4v-4h-2.59zM6.59 8L8 6.59V4H4c-1.11 0-2 .89-2 2v2zM6 14v-4H2v4zm2 3.41L6.59 16H2v2c0 1.11.89 2 2 2h4zM17.41 16L16 17.41V20h4c1.11 0 2-.89 2-2v-2z"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
class="secondary-link has-text-grey"
|
||||
href="/liabilities/credit_cards/{encodeURIComponent(creditCard.account)}"
|
||||
>
|
||||
<span class="custom-icon">{iconText(creditCard.account)}</span>
|
||||
<span class="ml-1">{restName(restName(creditCard.account))}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col">
|
||||
{#if bill}
|
||||
<div class="is-size-7">
|
||||
<span class="has-text-grey">Amount Due</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="is-size-4 has-text-grey-dark">{formatCurrency(bill.closingBalance)}</span>
|
||||
</div>
|
||||
<div class="is-size-7 has-text-grey">
|
||||
<DueDate dueDate={bill.dueDate} paidDate={bill.paidDate} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="is-size-7">
|
||||
<span class="has-text-grey">Balance</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="is-size-4 has-text-grey-dark">{formatCurrency(creditCard.balance)}</span>
|
||||
<span class="is-size-7 has-text-grey"
|
||||
>{formatPercentage(creditCard.balance / creditCard.creditLimit)} of {formatCurrency(
|
||||
creditCard.creditLimit
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-flex justify-between items-end">
|
||||
<div class="opacity-25 has-text-weight-bold is-size-5">
|
||||
* * * * {creditCard.number}
|
||||
</div>
|
||||
<div class="opacity-15">
|
||||
<CreditCardNetwork size={48} name={creditCard.network} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,119 @@
|
|||
<script lang="ts">
|
||||
export let size: number;
|
||||
export let name: string;
|
||||
$: multiplier =
|
||||
{
|
||||
rupay: 1.3,
|
||||
discover: 1.7
|
||||
}[name] || 1;
|
||||
</script>
|
||||
|
||||
{#if name == "visa"}
|
||||
<div style="margin: -{size / 2.9}px 0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M9.112 8.262L5.97 15.758H3.92L2.374 9.775c-.094-.368-.175-.503-.461-.658C1.447 8.864.677 8.627 0 8.479l.046-.217h3.3a.904.904 0 0 1 .894.764l.817 4.338l2.018-5.102zm8.033 5.049c.008-1.979-2.736-2.088-2.717-2.972c.006-.269.262-.555.822-.628a3.66 3.66 0 0 1 1.913.336l.34-1.59a5.207 5.207 0 0 0-1.814-.333c-1.917 0-3.266 1.02-3.278 2.479c-.012 1.079.963 1.68 1.698 2.04c.756.367 1.01.603 1.006.931c-.005.504-.602.725-1.16.734c-.975.015-1.54-.263-1.992-.473l-.351 1.642c.453.208 1.289.39 2.156.398c2.037 0 3.37-1.006 3.377-2.564m5.061 2.447H24l-1.565-7.496h-1.656a.883.883 0 0 0-.826.55l-2.909 6.946h2.036l.405-1.12h2.488zm-2.163-2.656l1.02-2.815l.588 2.815zm-8.16-4.84l-1.603 7.496H8.34l1.605-7.496z"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if name == "mastercard"}
|
||||
<div style="margin: -{size / 2.8}px 0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M15.273 18.728A6.728 6.728 0 1 1 22 11.999V12a6.735 6.735 0 0 1-6.727 6.728"
|
||||
opacity=".5"
|
||||
/><path
|
||||
fill="currentColor"
|
||||
d="M8.727 18.728A6.728 6.728 0 1 1 15.455 12a6.735 6.735 0 0 1-6.728 6.728"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if name == "dinersclub"}
|
||||
<div style="margin: -{size / 6}px 0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size * 0.65}
|
||||
height={size * 0.65}
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M16.506 11.982a6.026 6.026 0 0 0-3.866-5.618V17.6a6.025 6.025 0 0 0 3.866-5.618M8.33 17.598V6.365a6.03 6.03 0 0 0-3.863 5.617a6.028 6.028 0 0 0 3.863 5.616m2.156-15.113A9.497 9.497 0 0 0 .99 11.982a9.495 9.495 0 0 0 9.495 9.494a9.495 9.495 0 0 0 9.496-9.494a9.499 9.499 0 0 0-9.496-9.497Zm-.023 19.888C4.723 22.4 0 17.75 0 12.09C0 5.905 4.723 1.626 10.463 1.627h2.69C18.822 1.627 24 5.903 24 12.09c0 5.658-5.176 10.283-10.848 10.283"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if name == "amex"}
|
||||
<div style="margin: -{size / 2.1}px 0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size * 1.2}
|
||||
height={size * 1.2}
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="m4.314 11.965l-.82-1.997l-.815 1.997zm7.859 2.161l-.005-3.922l-1.736 3.922h-1.05L7.64 10.2v3.926H5.206l-.46-1.117H2.253l-.465 1.117h-1.3l2.144-5.008H4.41l2.036 4.742V9.118H8.4l1.567 3.397l1.439-3.397H13.4v5.008zm3.133-1.024v-.997h2.628v-1.022h-2.628v-.911h3.001l1.31 1.46l-1.368 1.47zm8.111 1.044h-1.556l-1.474-1.659l-1.532 1.659h-4.742v-5.01h4.815l1.473 1.642l1.523-1.642h1.564l-2.327 2.505z"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if name == "rupay"}
|
||||
<div style="margin: -{size / 1.9}px -{size * 1.7}px">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 333334 199007"
|
||||
width={size * 3.8 * multiplier}
|
||||
height={size * multiplier}
|
||||
shape-rendering="geometricPrecision"
|
||||
text-rendering="geometricPrecision"
|
||||
image-rendering="optimizeQuality"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
><path
|
||||
d="M214088 83928h13199v20970l11418-20970h12113l-24422 42395s-2267 3556-5079 5437c-2310 1547-5151 1477-6019 1540-4824-42-10643-55-10643-55l2807-10106 4542-8s2079-212 2882-1237c765-977 1156-1954 1156-3387 0-2148-1954-34580-1954-34580zM76939 88116c-1837 4256-7533 3772-7533 3772l-6632-31 2421-9013s5933 22 8843 22c3115 0 4088 2502 2902 5249zm15073-6142c1129-8943-6741-11201-15250-11201H54402l-13199 48105h14208l4436-16333 7970 65s3289-191 3354 2898c69 3295-2442 9345-2280 13370h14596l-32-1281s-1213-322-1078-2026c56-709 839-2953 1864-5979 618-1334 1550-4499 1464-7076-107-3217-2126-4710-5037-5754 9074-2128 11342-14787 11343-14787zm3224 1954h12959l-5337 20533s-1331 4579 2952 4932c3384 280 5902-3758 6727-6493 1084-3592 5296-18973 5296-18973h13351l-10159 34950h-11657l1432-4993s-5947 7252-14783 6382c-7854-771-8531-6468-7170-13575 668-3489 6389-22763 6389-22763zm66557 4298c-1849 5158-6944 4550-6944 4550l-6956 2 2746-10220s4403 23 7311 23c3560 0 4852 2828 3843 5644zm14609-4776c1130-8944-5687-12678-14197-12678h-22359l-13198 48105h14208l3995-14831 11320 69s17530 742 20232-20665zm11945 28307c-2220 563-4912 869-5446-1148-1466-5527 11474-7145 11474-7145 88 5036-4325 7859-6029 8293zm19575-10116c1707-5814 3865-11319 2128-14370-2660-4670-7468-5080-14502-5080-7771 0-17365 1476-20492 11810h12937s1179-3892 6035-3647c4298 217 4063 3174 2479 4809-2778 2865-10450 1276-18947 4224-7424 2576-10022 12338-8409 16234 1563 3778 4474 4259 8401 4645 6307 620 11143-2897 13394-4961 0 2292 60 3572 60 3572h13614l-33-1281s-1213-322-1078-2026c98-1248 2450-7252 4412-13929z"
|
||||
fill="currentColor"
|
||||
/><path fill="currentColor" d="M267751 75852l-15239 53011 28524-26506z" /><path
|
||||
fill="currentColor"
|
||||
d="M257982 75852l-15239 53011 28525-26506z"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if name == "jcb"}
|
||||
<div style="margin: -{size / 2.6}px 0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size * multiplier}
|
||||
height={size * multiplier}
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M13.05 9.864c.972.074 1.726.367 2.355.685v-1.31s-1.258-.317-2.441-.368C8.838 8.686 7.669 10.305 7.669 12s1.17 3.314 5.295 3.13c1.183-.054 2.44-.37 2.44-.37v-1.309c-.619.308-1.382.611-2.354.683c-1.68.128-2.69-.69-2.69-2.134c0-1.445 1.01-2.261 2.69-2.135m7.685 4.122a1.48 1.48 0 0 1-.215.02h-1.8v-1.631h1.8c.057 0 .164.01.215.02a.806.806 0 0 1 .632.795a.804.804 0 0 1-.632.796M18.72 9.95h1.632c.059 0 .145.007.177.013a.736.736 0 0 1 .626.74a.735.735 0 0 1-.626.739a1.571 1.571 0 0 1-.178.013h-1.63zm3.499 1.985V11.9c.913-.133 1.415-.726 1.415-1.42c0-.883-.734-1.392-1.73-1.442c-.077-.003-.202-.01-.304-.01h-5.332v5.946h5.755c1.13 0 1.977-.604 1.977-1.547c0-.87-.772-1.422-1.781-1.491zm-17.864.68c0 .878-.591 1.53-1.666 1.53c-.917 0-1.817-.272-2.689-.694v1.309s1.402.383 3.191.383c2.971 0 3.837-1.125 3.837-2.529V9.027H4.354z"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if name == "discover"}
|
||||
<div style="margin: -{size / 1.4}px 0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size * multiplier}
|
||||
height={size * multiplier}
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M14.58 12a2.023 2.023 0 1 1-2.025-2.023h.002c1.118 0 2.023.906 2.023 2.023m-5.2-2.001c-1.124 0-2.025.884-2.025 1.99c0 1.118.878 1.984 2.007 1.984c.319 0 .593-.063.93-.221v-.873c-.296.297-.559.416-.895.416c-.747 0-1.277-.542-1.277-1.312c0-.73.547-1.306 1.243-1.306c.354 0 .622.126.93.428v-.873a1.898 1.898 0 0 0-.913-.233m-3.352 1.545c-.445-.165-.576-.273-.576-.479c0-.239.233-.422.553-.422c.222 0 .405.091.598.308l.388-.508a1.665 1.665 0 0 0-1.117-.422c-.673 0-1.186.467-1.186 1.089c0 .524.239.792.936 1.043c.291.103.438.171.513.217a.456.456 0 0 1 .222.394c0 .308-.245.536-.576.536c-.354 0-.639-.177-.809-.507l-.479.461c.342.502.752.724 1.317.724c.771 0 1.311-.513 1.311-1.249c-.002-.603-.252-.876-1.095-1.185M24 10.3a.29.29 0 0 1-.288.291a.29.29 0 0 1-.291-.291v-.003A.29.29 0 1 1 24 10.3m-.059.001a.235.235 0 0 0-.231-.239a.234.234 0 0 0-.232.239c0 .132.104.239.232.239a.235.235 0 0 0 .231-.239M3.472 13.887h.742v-3.803h-.742zm12.702-1.248l-1.014-2.554h-.81l1.614 3.9h.399l1.643-3.9h-.804zm2.166 1.248h2.104v-.644h-1.362v-1.027h1.312v-.644h-1.312v-.844h1.362v-.644H18.34zm5.409-3.557l.11.138h-.097l-.094-.13v.13h-.08v-.334h.107c.081 0 .126.036.126.103c.001.046-.025.08-.072.093m-.006-.092c0-.029-.021-.043-.06-.043h-.014v.087h.014c.039 0 .06-.014.06-.044m-1.228 2.047l1.197 1.602H22.8l-1.027-1.528h-.097v1.528h-.741v-3.803h1.1c.855 0 1.346.411 1.346 1.123c0 .583-.308.965-.866 1.078m.103-1.038c0-.37-.251-.563-.713-.563h-.228v1.152h.217c.473-.001.724-.207.724-.589m-19.487.742a1.91 1.91 0 0 1-.69 1.46c-.365.303-.781.439-1.357.439H.001v-3.803H1.09c1.202 0 2.041.781 2.041 1.904m-.764-.006c0-.364-.154-.718-.411-.947c-.245-.222-.536-.308-1.015-.308H.742v2.515h.199c.479 0 .782-.092 1.015-.302c.256-.228.411-.593.411-.958"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
{/if}
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { dueDateIcon } from "$lib/utils";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export let dueDate: dayjs.Dayjs;
|
||||
export let paidDate: dayjs.Dayjs;
|
||||
|
||||
$: icon = dueDateIcon(dueDate, paidDate);
|
||||
</script>
|
||||
|
||||
<span title="due on {dueDate.format('DD MMM YYYY')}">
|
||||
<span class="icon is-small {icon.color}">
|
||||
<i class="fas {icon.icon}" />
|
||||
</span>
|
||||
{#if paidDate}
|
||||
<span>paid on {paidDate.format("DD MMM YYYY")}</span>
|
||||
{:else}
|
||||
<span>due {dueDate.fromNow()}</span>
|
||||
{/if}
|
||||
</span>
|
|
@ -97,6 +97,7 @@
|
|||
href: "/liabilities",
|
||||
children: [
|
||||
{ label: "Balance", href: "/balance" },
|
||||
{ label: "Credit Cards", href: "/credit_cards", help: "credit-cards" },
|
||||
{ label: "Repayment", href: "/repayment" },
|
||||
{ label: "Interest", href: "/interest" }
|
||||
]
|
||||
|
|
|
@ -5,7 +5,8 @@ import {
|
|||
transactionTotal,
|
||||
type Transaction,
|
||||
type TransactionSchedule,
|
||||
type TransactionSequence
|
||||
type TransactionSequence,
|
||||
dueDateIcon
|
||||
} from "./utils";
|
||||
import dayjs from "dayjs";
|
||||
import { parse, type CronExprs } from "@datasert/cronjs-parser";
|
||||
|
@ -148,32 +149,7 @@ export function nextUnpaidSchedule(ts: TransactionSequence) {
|
|||
}
|
||||
|
||||
export function scheduleIcon(schedule: TransactionSchedule) {
|
||||
let icon = "fa-circle-check";
|
||||
let glyph = iconGlyph("fa6-solid:circle-check");
|
||||
let color = "has-text-success";
|
||||
let svgColor = "svg-text-success";
|
||||
|
||||
if (!schedule.actual) {
|
||||
if (schedule.scheduled.isBefore(now(), "day")) {
|
||||
color = "has-text-danger";
|
||||
icon = "fa-exclamation-triangle";
|
||||
glyph = iconGlyph("fa6-solid:triangle-exclamation");
|
||||
svgColor = "svg-text-danger";
|
||||
} else {
|
||||
color = "has-text-grey";
|
||||
svgColor = "svg-text-grey";
|
||||
}
|
||||
} else {
|
||||
if (schedule.actual.isSameOrBefore(schedule.scheduled, "day")) {
|
||||
color = "has-text-success";
|
||||
svgColor = "svg-text-success";
|
||||
} else {
|
||||
color = "has-text-warning-dark";
|
||||
svgColor = "svg-text-warning-dark";
|
||||
}
|
||||
}
|
||||
|
||||
return { icon, color, svgColor, glyph };
|
||||
return dueDateIcon(schedule.scheduled, schedule.actual);
|
||||
}
|
||||
|
||||
export function intervalText(ts: TransactionSequence) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { obscure } from "../persisted_store";
|
|||
import { error } from "@sveltejs/kit";
|
||||
import { goto } from "$app/navigation";
|
||||
import chroma from "chroma-js";
|
||||
import { iconGlyph } from "./icon";
|
||||
|
||||
export interface AutoCompleteItem {
|
||||
label: string;
|
||||
|
@ -481,6 +482,28 @@ export interface Log {
|
|||
msg: string;
|
||||
}
|
||||
|
||||
export interface CreditCardBill {
|
||||
openingBalance: number;
|
||||
closingBalance: number;
|
||||
debits: number;
|
||||
credits: number;
|
||||
statementStartDate: dayjs.Dayjs;
|
||||
statementEndDate: dayjs.Dayjs;
|
||||
dueDate: dayjs.Dayjs;
|
||||
paidDate: dayjs.Dayjs;
|
||||
postings: Posting[];
|
||||
transactions: Transaction[];
|
||||
}
|
||||
|
||||
export interface CreditCardSummary {
|
||||
account: string;
|
||||
network: string;
|
||||
number: string;
|
||||
balance: number;
|
||||
bills: CreditCardBill[];
|
||||
creditLimit: number;
|
||||
}
|
||||
|
||||
export interface GoalSummary {
|
||||
type: string;
|
||||
name: string;
|
||||
|
@ -609,6 +632,14 @@ export function ajax(route: "/api/liabilities/interest"): Promise<{
|
|||
interest_timeline_breakdown: Interest[];
|
||||
}>;
|
||||
|
||||
export function ajax(route: "/api/credit_cards"): Promise<{ creditCards: CreditCardSummary[] }>;
|
||||
|
||||
export function ajax(
|
||||
route: "/api/credit_cards/:account",
|
||||
options?: RequestOptions,
|
||||
params?: Record<string, string>
|
||||
): Promise<{ creditCard: CreditCardSummary; found: boolean }>;
|
||||
|
||||
export function ajax(route: "/api/goals"): Promise<{ goals: GoalSummary[] }>;
|
||||
export function ajax(
|
||||
route: "/api/goals/retirement/:name",
|
||||
|
@ -764,7 +795,7 @@ export async function ajax(
|
|||
return JSON.parse(body, (key, value) => {
|
||||
if (
|
||||
_.isString(value) &&
|
||||
/date|time|now/.test(key) &&
|
||||
/Date|date|time|now/.test(key) &&
|
||||
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$/.test(
|
||||
value
|
||||
)
|
||||
|
@ -1206,3 +1237,32 @@ export function asTransaction(p: Posting): Transaction {
|
|||
export function svgUrl(identifier: string) {
|
||||
return `url(${new URL("#" + identifier, window.location.toString())})`;
|
||||
}
|
||||
|
||||
export function dueDateIcon(dueDate: dayjs.Dayjs, clearedDate: dayjs.Dayjs) {
|
||||
let icon = "fa-circle-check";
|
||||
let glyph = iconGlyph("fa6-solid:circle-check");
|
||||
let color = "has-text-success";
|
||||
let svgColor = "svg-text-success";
|
||||
|
||||
if (!clearedDate) {
|
||||
if (dueDate.isBefore(now(), "day")) {
|
||||
color = "has-text-danger";
|
||||
icon = "fa-exclamation-triangle";
|
||||
glyph = iconGlyph("fa6-solid:triangle-exclamation");
|
||||
svgColor = "svg-text-danger";
|
||||
} else {
|
||||
color = "has-text-grey";
|
||||
svgColor = "svg-text-grey";
|
||||
}
|
||||
} else {
|
||||
if (clearedDate.isSameOrBefore(dueDate, "day")) {
|
||||
color = "has-text-success";
|
||||
svgColor = "svg-text-success";
|
||||
} else {
|
||||
color = "has-text-warning-dark";
|
||||
svgColor = "svg-text-warning-dark";
|
||||
}
|
||||
}
|
||||
|
||||
return { icon, color, svgColor, glyph };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import CreditCardCard from "$lib/components/CreditCardCard.svelte";
|
||||
import ZeroState from "$lib/components/ZeroState.svelte";
|
||||
import { ajax, helpUrl, type CreditCardSummary } from "$lib/utils";
|
||||
import _ from "lodash";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let isEmpty = false;
|
||||
let creditCards: CreditCardSummary[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
({ creditCards } = await ajax("/api/credit_cards"));
|
||||
if (_.isEmpty(creditCards)) {
|
||||
isEmpty = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="section">
|
||||
<div class="container is-fluid">
|
||||
<div class="columns flex-wrap">
|
||||
<div class="column is-12">
|
||||
<div class="credit-card-container">
|
||||
{#each creditCards as creditCard}
|
||||
<CreditCardCard {creditCard} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns flex-wrap">
|
||||
<div class="column is-12">
|
||||
<ZeroState item={!isEmpty}>
|
||||
<strong>Oops!</strong> You haven't configured any credit cards yet. Checkout the
|
||||
<a href={helpUrl("credit-card")}>docs</a> page to get started.
|
||||
</ZeroState>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,181 @@
|
|||
<script lang="ts">
|
||||
import CreditCardCard from "$lib/components/CreditCardCard.svelte";
|
||||
import {
|
||||
ajax,
|
||||
formatCurrency,
|
||||
type CreditCardBill,
|
||||
type CreditCardSummary,
|
||||
formatPercentage
|
||||
} from "$lib/utils";
|
||||
import _, { now } from "lodash";
|
||||
import { onMount } from "svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { MasonryGrid } from "@egjs/svelte-grid";
|
||||
import TransactionCard from "$lib/components/TransactionCard.svelte";
|
||||
import LevelItem from "$lib/components/LevelItem.svelte";
|
||||
import COLORS from "$lib/colors";
|
||||
import { iconify } from "$lib/icon";
|
||||
import DueDate from "$lib/components/DueDate.svelte";
|
||||
let UntypedMasonryGrid = MasonryGrid as any;
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let creditCard: CreditCardSummary;
|
||||
let currentBill: CreditCardBill;
|
||||
let found = false;
|
||||
let small = true;
|
||||
|
||||
function lastBill(creditCard: CreditCardSummary): CreditCardBill {
|
||||
return _.find(_.reverse(_.clone(creditCard.bills)), (b) => {
|
||||
return b.statementEndDate.isSameOrBefore(now());
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
({ creditCard, found } = await ajax("/api/credit_cards/:account", null, data));
|
||||
currentBill = lastBill(creditCard);
|
||||
if (!found) {
|
||||
redirect(307, `/liabilities/credit_cards`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="section">
|
||||
<div class="container is-fluid">
|
||||
<div class="columns flex-wrap">
|
||||
<div class="column is-3-widescreen is-4">
|
||||
{#if creditCard}
|
||||
<div class="flex mb-4">
|
||||
<CreditCardCard {creditCard} />
|
||||
</div>
|
||||
|
||||
<nav class="level grid-2">
|
||||
<LevelItem
|
||||
narrow
|
||||
small
|
||||
title="Available Credit"
|
||||
color={COLORS.neutral}
|
||||
value={formatCurrency(Math.max(creditCard.creditLimit - creditCard.balance, 0))}
|
||||
/>
|
||||
|
||||
<LevelItem
|
||||
narrow
|
||||
small
|
||||
title="Credit Usage"
|
||||
color={COLORS.neutral}
|
||||
value={formatPercentage(creditCard.balance / creditCard.creditLimit, 2)}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<nav class="level grid-2">
|
||||
<LevelItem
|
||||
narrow
|
||||
small
|
||||
title="Statement Count"
|
||||
color={COLORS.neutral}
|
||||
value={creditCard.bills.length.toString()}
|
||||
/>
|
||||
<LevelItem
|
||||
narrow
|
||||
small
|
||||
title="Transaction Count"
|
||||
color={COLORS.neutral}
|
||||
value={_.sumBy(creditCard.bills, (b) => b.transactions.length).toString()}
|
||||
/>
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="column is-9-widescreen is-8">
|
||||
{#if currentBill}
|
||||
<div class="flex flex-wrap gap-4 mb-4">
|
||||
<div class="box py-2 m-0 flex-grow" style="border: 1px solid transparent">
|
||||
<div
|
||||
class="is-flex mr-2 is-align-items-baseline overflow-x-scroll"
|
||||
style="min-width: fit-content"
|
||||
>
|
||||
<div class="ml-3 custom-icon is-size-5">
|
||||
<span>{iconify(creditCard.account)}</span>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<span class="mr-1 is-size-7 has-text-grey">Payment</span>
|
||||
<span
|
||||
><DueDate dueDate={currentBill.dueDate} paidDate={currentBill.paidDate} /></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-text-right">
|
||||
<div class="select is-medium">
|
||||
<select bind:value={currentBill}>
|
||||
{#each _.reverse(_.clone(creditCard.bills)) as bill}
|
||||
<option value={bill}
|
||||
>{bill.statementStartDate.format("DD MMM YYYY")} — {bill.statementEndDate.format(
|
||||
"DD MMM YYYY"
|
||||
)}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="level flex gap-4 overflow-x-scroll" style="justify-content: start;">
|
||||
<LevelItem
|
||||
{small}
|
||||
narrow
|
||||
title="Opening Balance"
|
||||
color={COLORS.neutral}
|
||||
value={formatCurrency(currentBill.openingBalance)}
|
||||
/>
|
||||
<div class="level-item is-narrow">
|
||||
<span class="icon is-size-3">
|
||||
<i class="fas fa-plus" />
|
||||
</span>
|
||||
</div>
|
||||
<LevelItem
|
||||
{small}
|
||||
narrow
|
||||
title="Debits"
|
||||
color={COLORS.expenses}
|
||||
value={formatCurrency(currentBill.debits)}
|
||||
/>
|
||||
<div class="level-item is-narrow">
|
||||
<span class="icon is-size-3">
|
||||
<i class="fas fa-minus" />
|
||||
</span>
|
||||
</div>
|
||||
<LevelItem
|
||||
{small}
|
||||
narrow
|
||||
title="Credits"
|
||||
color={COLORS.income}
|
||||
value={formatCurrency(currentBill.credits)}
|
||||
/>
|
||||
<div class="level-item is-narrow">
|
||||
<span class="icon is-size-3">
|
||||
<i class="fas fa-equals" />
|
||||
</span>
|
||||
</div>
|
||||
<LevelItem
|
||||
{small}
|
||||
narrow
|
||||
title="Amount Due"
|
||||
color={COLORS.liabilities}
|
||||
value={formatCurrency(currentBill.closingBalance)}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<div>
|
||||
<UntypedMasonryGrid gap={10} maxStretchColumnSize={500} align="stretch">
|
||||
{#each currentBill.transactions as t}
|
||||
<div class="mr-3 is-flex-grow-1">
|
||||
<TransactionCard {t} />
|
||||
</div>
|
||||
{/each}
|
||||
</UntypedMasonryGrid>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,7 @@
|
|||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
return {
|
||||
account: params.slug
|
||||
};
|
||||
}) satisfies PageLoad;
|
|
@ -63,14 +63,6 @@
|
|||
|
||||
<section class="section">
|
||||
<div class="container is-fluid">
|
||||
<div class="columns flex-wrap">
|
||||
<div class="column is-12">
|
||||
<ZeroState item={!isEmpty}>
|
||||
<strong>Oops!</strong> You haven't configured any goals yet. Checkout the
|
||||
<a href={helpUrl("goals")}>docs</a> page to get started.
|
||||
</ZeroState>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="columns flex-wrap"
|
||||
use:dndzone={{ items: goals, dropTargetStyle: {}, flipDurationMs: 300 }}
|
||||
|
@ -83,5 +75,13 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="columns flex-wrap">
|
||||
<div class="column is-12">
|
||||
<ZeroState item={!isEmpty}>
|
||||
<strong>Oops!</strong> You haven't configured any goals yet. Checkout the
|
||||
<a href={helpUrl("goals")}>docs</a> page to get started.
|
||||
</ZeroState>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"retirement": [],
|
||||
"savings": []
|
||||
},
|
||||
"user_accounts": []
|
||||
"user_accounts": [],
|
||||
"credit_cards": []
|
||||
},
|
||||
"now": "2022-02-07T00:00:00Z",
|
||||
"schema": {
|
||||
|
@ -53,7 +54,7 @@
|
|||
"ui:widget": "icon"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the account",
|
||||
"description": "Name of the account",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -157,7 +158,7 @@
|
|||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the commodity",
|
||||
"description": "Name of the commodity",
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
|
@ -221,6 +222,76 @@
|
|||
],
|
||||
"type": "array"
|
||||
},
|
||||
"credit_cards": {
|
||||
"default": [
|
||||
{
|
||||
"account": "Liabilities:CreditCard:Chase",
|
||||
"credit_limit": 100000,
|
||||
"due_day": 15,
|
||||
"statement_end_day": 28
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"description": "Name of the credit card account",
|
||||
"type": "string"
|
||||
},
|
||||
"credit_limit": {
|
||||
"description": "Credit limit of the card",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"due_day": {
|
||||
"description": "Due day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"network": {
|
||||
"description": "Network of the card",
|
||||
"enum": [
|
||||
"visa",
|
||||
"mastercard",
|
||||
"dinersclub",
|
||||
"amex",
|
||||
"rupay",
|
||||
"jcb",
|
||||
"discover"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"number": {
|
||||
"description": "Last 4 digits of the card number",
|
||||
"maxLength": 4,
|
||||
"minLength": 4,
|
||||
"pattern": "^[0-9]{4}$",
|
||||
"type": "string"
|
||||
},
|
||||
"statement_end_day": {
|
||||
"description": "Statement end day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"account",
|
||||
"credit_limit",
|
||||
"statement_end_day",
|
||||
"due_day",
|
||||
"network",
|
||||
"number"
|
||||
],
|
||||
"type": "object",
|
||||
"ui:header": "account"
|
||||
},
|
||||
"itemsUniqueProperties": [
|
||||
"account"
|
||||
],
|
||||
"type": "array"
|
||||
},
|
||||
"db_path": {
|
||||
"description": "Path to your database file. It can be absolute or relative to the configuration file. The database file will be created if it does not exist.",
|
||||
"type": "string"
|
||||
|
@ -430,7 +501,7 @@
|
|||
"ui:widget": "textarea"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the template",
|
||||
"description": "Name of the template",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"retirement": [],
|
||||
"savings": []
|
||||
},
|
||||
"user_accounts": []
|
||||
"user_accounts": [],
|
||||
"credit_cards": []
|
||||
},
|
||||
"now": "2022-02-07T00:00:00Z",
|
||||
"schema": {
|
||||
|
@ -53,7 +54,7 @@
|
|||
"ui:widget": "icon"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the account",
|
||||
"description": "Name of the account",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -157,7 +158,7 @@
|
|||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the commodity",
|
||||
"description": "Name of the commodity",
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
|
@ -221,6 +222,76 @@
|
|||
],
|
||||
"type": "array"
|
||||
},
|
||||
"credit_cards": {
|
||||
"default": [
|
||||
{
|
||||
"account": "Liabilities:CreditCard:Chase",
|
||||
"credit_limit": 100000,
|
||||
"due_day": 15,
|
||||
"statement_end_day": 28
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"description": "Name of the credit card account",
|
||||
"type": "string"
|
||||
},
|
||||
"credit_limit": {
|
||||
"description": "Credit limit of the card",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"due_day": {
|
||||
"description": "Due day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"network": {
|
||||
"description": "Network of the card",
|
||||
"enum": [
|
||||
"visa",
|
||||
"mastercard",
|
||||
"dinersclub",
|
||||
"amex",
|
||||
"rupay",
|
||||
"jcb",
|
||||
"discover"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"number": {
|
||||
"description": "Last 4 digits of the card number",
|
||||
"maxLength": 4,
|
||||
"minLength": 4,
|
||||
"pattern": "^[0-9]{4}$",
|
||||
"type": "string"
|
||||
},
|
||||
"statement_end_day": {
|
||||
"description": "Statement end day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"account",
|
||||
"credit_limit",
|
||||
"statement_end_day",
|
||||
"due_day",
|
||||
"network",
|
||||
"number"
|
||||
],
|
||||
"type": "object",
|
||||
"ui:header": "account"
|
||||
},
|
||||
"itemsUniqueProperties": [
|
||||
"account"
|
||||
],
|
||||
"type": "array"
|
||||
},
|
||||
"db_path": {
|
||||
"description": "Path to your database file. It can be absolute or relative to the configuration file. The database file will be created if it does not exist.",
|
||||
"type": "string"
|
||||
|
@ -430,7 +501,7 @@
|
|||
"ui:widget": "textarea"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the template",
|
||||
"description": "Name of the template",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
"marketAmount": 0,
|
||||
"balanceUnits": -100,
|
||||
"latestPrice": 0,
|
||||
"xirr": 149.2,
|
||||
"xirr": 167.56,
|
||||
"gainAmount": 27,
|
||||
"absoluteReturn": 0.0027
|
||||
},
|
||||
|
|
|
@ -36,7 +36,8 @@
|
|||
"retirement": [],
|
||||
"savings": []
|
||||
},
|
||||
"user_accounts": []
|
||||
"user_accounts": [],
|
||||
"credit_cards": []
|
||||
},
|
||||
"now": "2022-02-07T00:00:00Z",
|
||||
"schema": {
|
||||
|
@ -61,7 +62,7 @@
|
|||
"ui:widget": "icon"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the account",
|
||||
"description": "Name of the account",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -165,7 +166,7 @@
|
|||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the commodity",
|
||||
"description": "Name of the commodity",
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
|
@ -229,6 +230,76 @@
|
|||
],
|
||||
"type": "array"
|
||||
},
|
||||
"credit_cards": {
|
||||
"default": [
|
||||
{
|
||||
"account": "Liabilities:CreditCard:Chase",
|
||||
"credit_limit": 100000,
|
||||
"due_day": 15,
|
||||
"statement_end_day": 28
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"description": "Name of the credit card account",
|
||||
"type": "string"
|
||||
},
|
||||
"credit_limit": {
|
||||
"description": "Credit limit of the card",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"due_day": {
|
||||
"description": "Due day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"network": {
|
||||
"description": "Network of the card",
|
||||
"enum": [
|
||||
"visa",
|
||||
"mastercard",
|
||||
"dinersclub",
|
||||
"amex",
|
||||
"rupay",
|
||||
"jcb",
|
||||
"discover"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"number": {
|
||||
"description": "Last 4 digits of the card number",
|
||||
"maxLength": 4,
|
||||
"minLength": 4,
|
||||
"pattern": "^[0-9]{4}$",
|
||||
"type": "string"
|
||||
},
|
||||
"statement_end_day": {
|
||||
"description": "Statement end day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"account",
|
||||
"credit_limit",
|
||||
"statement_end_day",
|
||||
"due_day",
|
||||
"network",
|
||||
"number"
|
||||
],
|
||||
"type": "object",
|
||||
"ui:header": "account"
|
||||
},
|
||||
"itemsUniqueProperties": [
|
||||
"account"
|
||||
],
|
||||
"type": "array"
|
||||
},
|
||||
"db_path": {
|
||||
"description": "Path to your database file. It can be absolute or relative to the configuration file. The database file will be created if it does not exist.",
|
||||
"type": "string"
|
||||
|
@ -438,7 +509,7 @@
|
|||
"ui:widget": "textarea"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the template",
|
||||
"description": "Name of the template",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
"balanceUnits": 0,
|
||||
"netInvestmentAmount": -27
|
||||
},
|
||||
"xirr": 149.2,
|
||||
"xirr": 167.56,
|
||||
"postings": [
|
||||
{
|
||||
"id": 11,
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"marketAmount": 0,
|
||||
"balanceUnits": -100,
|
||||
"latestPrice": 0,
|
||||
"xirr": 151.56,
|
||||
"xirr": 170.5,
|
||||
"gainAmount": 27.3,
|
||||
"absoluteReturn": 0.00273
|
||||
},
|
||||
|
|
|
@ -35,7 +35,8 @@
|
|||
"retirement": [],
|
||||
"savings": []
|
||||
},
|
||||
"user_accounts": []
|
||||
"user_accounts": [],
|
||||
"credit_cards": []
|
||||
},
|
||||
"now": "2022-02-07T00:00:00Z",
|
||||
"schema": {
|
||||
|
@ -60,7 +61,7 @@
|
|||
"ui:widget": "icon"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the account",
|
||||
"description": "Name of the account",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -164,7 +165,7 @@
|
|||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the commodity",
|
||||
"description": "Name of the commodity",
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
|
@ -228,6 +229,76 @@
|
|||
],
|
||||
"type": "array"
|
||||
},
|
||||
"credit_cards": {
|
||||
"default": [
|
||||
{
|
||||
"account": "Liabilities:CreditCard:Chase",
|
||||
"credit_limit": 100000,
|
||||
"due_day": 15,
|
||||
"statement_end_day": 28
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"description": "Name of the credit card account",
|
||||
"type": "string"
|
||||
},
|
||||
"credit_limit": {
|
||||
"description": "Credit limit of the card",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"due_day": {
|
||||
"description": "Due day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"network": {
|
||||
"description": "Network of the card",
|
||||
"enum": [
|
||||
"visa",
|
||||
"mastercard",
|
||||
"dinersclub",
|
||||
"amex",
|
||||
"rupay",
|
||||
"jcb",
|
||||
"discover"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"number": {
|
||||
"description": "Last 4 digits of the card number",
|
||||
"maxLength": 4,
|
||||
"minLength": 4,
|
||||
"pattern": "^[0-9]{4}$",
|
||||
"type": "string"
|
||||
},
|
||||
"statement_end_day": {
|
||||
"description": "Statement end day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"account",
|
||||
"credit_limit",
|
||||
"statement_end_day",
|
||||
"due_day",
|
||||
"network",
|
||||
"number"
|
||||
],
|
||||
"type": "object",
|
||||
"ui:header": "account"
|
||||
},
|
||||
"itemsUniqueProperties": [
|
||||
"account"
|
||||
],
|
||||
"type": "array"
|
||||
},
|
||||
"db_path": {
|
||||
"description": "Path to your database file. It can be absolute or relative to the configuration file. The database file will be created if it does not exist.",
|
||||
"type": "string"
|
||||
|
@ -437,7 +508,7 @@
|
|||
"ui:widget": "textarea"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the template",
|
||||
"description": "Name of the template",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"balanceUnits": 0,
|
||||
"netInvestmentAmount": -27.3
|
||||
},
|
||||
"xirr": 151.56,
|
||||
"xirr": 170.5,
|
||||
"postings": [
|
||||
{
|
||||
"id": 15,
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"marketAmount": 0,
|
||||
"balanceUnits": -100,
|
||||
"latestPrice": 0,
|
||||
"xirr": 151.56,
|
||||
"xirr": 170.5,
|
||||
"gainAmount": 27.3,
|
||||
"absoluteReturn": 0.00273
|
||||
},
|
||||
|
|
|
@ -35,7 +35,8 @@
|
|||
"retirement": [],
|
||||
"savings": []
|
||||
},
|
||||
"user_accounts": []
|
||||
"user_accounts": [],
|
||||
"credit_cards": []
|
||||
},
|
||||
"now": "2022-02-07T00:00:00Z",
|
||||
"schema": {
|
||||
|
@ -60,7 +61,7 @@
|
|||
"ui:widget": "icon"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the account",
|
||||
"description": "Name of the account",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -164,7 +165,7 @@
|
|||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the commodity",
|
||||
"description": "Name of the commodity",
|
||||
"type": "string"
|
||||
},
|
||||
"price": {
|
||||
|
@ -228,6 +229,76 @@
|
|||
],
|
||||
"type": "array"
|
||||
},
|
||||
"credit_cards": {
|
||||
"default": [
|
||||
{
|
||||
"account": "Liabilities:CreditCard:Chase",
|
||||
"credit_limit": 100000,
|
||||
"due_day": 15,
|
||||
"statement_end_day": 28
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": {
|
||||
"description": "Name of the credit card account",
|
||||
"type": "string"
|
||||
},
|
||||
"credit_limit": {
|
||||
"description": "Credit limit of the card",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
},
|
||||
"due_day": {
|
||||
"description": "Due day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"network": {
|
||||
"description": "Network of the card",
|
||||
"enum": [
|
||||
"visa",
|
||||
"mastercard",
|
||||
"dinersclub",
|
||||
"amex",
|
||||
"rupay",
|
||||
"jcb",
|
||||
"discover"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"number": {
|
||||
"description": "Last 4 digits of the card number",
|
||||
"maxLength": 4,
|
||||
"minLength": 4,
|
||||
"pattern": "^[0-9]{4}$",
|
||||
"type": "string"
|
||||
},
|
||||
"statement_end_day": {
|
||||
"description": "Statement end day of the card",
|
||||
"maximum": 31,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"account",
|
||||
"credit_limit",
|
||||
"statement_end_day",
|
||||
"due_day",
|
||||
"network",
|
||||
"number"
|
||||
],
|
||||
"type": "object",
|
||||
"ui:header": "account"
|
||||
},
|
||||
"itemsUniqueProperties": [
|
||||
"account"
|
||||
],
|
||||
"type": "array"
|
||||
},
|
||||
"db_path": {
|
||||
"description": "Path to your database file. It can be absolute or relative to the configuration file. The database file will be created if it does not exist.",
|
||||
"type": "string"
|
||||
|
@ -437,7 +508,7 @@
|
|||
"ui:widget": "textarea"
|
||||
},
|
||||
"name": {
|
||||
"description": "name of the template",
|
||||
"description": "Name of the template",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"balanceUnits": 0,
|
||||
"netInvestmentAmount": -27.3
|
||||
},
|
||||
"xirr": 151.56,
|
||||
"xirr": 170.5,
|
||||
"postings": [
|
||||
{
|
||||
"id": 11,
|
||||
|
|
Loading…
Reference in New Issue