[docs] add docs for goals
This commit is contained in:
parent
7f39975270
commit
e473f49e04
|
@ -15,6 +15,17 @@ configuration file named `paisa/paisa.yaml` inside User Documents folder. The
|
|||
default configuration is tuned for Indians, users from other countries
|
||||
would have to change the `default_currency` and `locale`.
|
||||
|
||||
### Accounts
|
||||
|
||||
In many places, paisa expects you to specify a list of accounts. You
|
||||
can type the full account name like `#!ledger
|
||||
Account:Equity:APPL`. Paisa also supports wildcard `*`, you can use
|
||||
`#!ledger Account:Equity:*` to represent all accounts under
|
||||
Equity. It's also possible to use negation. `#!ledger !Expenses:Tax`
|
||||
will match all accounts except Tax. If you use negation, then all the
|
||||
accounts should be negation. Don't mix negation with others, if done
|
||||
the behavior will be undefined.
|
||||
|
||||
```yaml
|
||||
# Path to your journal file. It can be absolute or relative to the
|
||||
# configuration file. The main journal file can refer other files using
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Goals
|
||||
|
||||
Goal helps you track your financial objective and progress. You can
|
||||
create a goal for any financial objective you have, such as saving for
|
||||
a vacation, or building an emergency fund or planning for retirement.
|
||||
|
||||
Paisa currently supports two types of goals:
|
||||
|
||||
1. [Retirement](./retirement.md)
|
||||
2. [Savings](./savings.md)
|
||||
|
||||
|
||||
More goal types will be added in the future. Feel free to create a
|
||||
[discussion](https://github.com/ananthakumaran/paisa/discussions) if you have any suggestions.
|
||||
|
||||
|
||||
### Create Goal
|
||||
|
||||
To add a new goal, go to the `Configuration` page and expand the goals
|
||||
section. You would see a list of goal types. Expand the type you want
|
||||
to add and click on the :fontawesome-solid-circle-plus: icon to add a
|
||||
new goal. The configuration details are available in the respective
|
||||
goal type pages.
|
|
@ -0,0 +1,56 @@
|
|||
# Retirement
|
||||
|
||||
Paisa will help you plan your retirement and track your progress. The
|
||||
first part is figuring out what should be your retirement corpus. This
|
||||
will be your target. Instead of specifying the amount explicitly, you
|
||||
can specify your expected yearly expenses and the safe withdrawal
|
||||
rate.
|
||||
|
||||
```yaml
|
||||
goals:
|
||||
retirement:
|
||||
- name: Retirement
|
||||
icon: mdi:palm-tree
|
||||
swr: 3.3
|
||||
yearly_expenses: 1100000
|
||||
```
|
||||
|
||||
If you use paisa to track expenses, instead of specifying the
|
||||
`yearly_expenses`, you can specify the list of accounts. Paisa will
|
||||
take the average of the last 3 year expenses
|
||||
|
||||
```yaml
|
||||
goals:
|
||||
retirement:
|
||||
- name: Retirement
|
||||
icon: mdi:palm-tree
|
||||
swr: 2
|
||||
expenses:
|
||||
- Expenses:Entertainment
|
||||
- Expenses:Gift
|
||||
- Expenses:Insurance
|
||||
- Expenses:Misc
|
||||
- Expenses:Shopping
|
||||
- Expenses:Utilities
|
||||
```
|
||||
|
||||
Now that the target is specified, you need to specify the list of
|
||||
accounts where you keep your retirement savings.
|
||||
|
||||
```yaml
|
||||
goals:
|
||||
retirement:
|
||||
- name: Retirement
|
||||
icon: mdi:palm-tree
|
||||
swr: 2
|
||||
expenses:
|
||||
- Expenses:Entertainment
|
||||
- Expenses:Gift
|
||||
- Expenses:Insurance
|
||||
- Expenses:Misc
|
||||
- Expenses:Shopping
|
||||
- Expenses:Utilities
|
||||
savings:
|
||||
- Assets:Equity:*
|
||||
- Assets:Debt:*
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
# Savings
|
||||
|
||||
Savings represents a general financial objective that you want to
|
||||
achieve, like buying a car, or a house, or a vacation.
|
||||
|
||||
```yaml
|
||||
goals:
|
||||
retirement:
|
||||
- name: House
|
||||
target: 5000000
|
||||
icon: fluent-emoji-high-contrast:house-with-garden
|
||||
accounts:
|
||||
- Assets:Equity:*
|
||||
- Assets:Debt:*
|
||||
```
|
||||
|
||||
Specify the target amount, and the accounts that you keep the money in.
|
|
@ -1,47 +0,0 @@
|
|||
# Retirement
|
||||
|
||||
Paisa will help you plan your retirement and track your progress. The
|
||||
first part is figuring out what should be your retirement corpus. This
|
||||
will be your target. Instead of specifying the amount explicitly, you
|
||||
can specify your expected yearly expenses and the safe withdrawal
|
||||
rate.
|
||||
|
||||
```yaml
|
||||
retirement:
|
||||
swr: 3.3
|
||||
yearly_expenses: 1100000
|
||||
```
|
||||
|
||||
If you use paisa to track expenses, instead of specifying the
|
||||
`yearly_expenses`, you can specify the list of accounts. Paisa will
|
||||
take the average of the last 3 year expenses
|
||||
|
||||
```yaml
|
||||
retirement:
|
||||
swr: 2
|
||||
expenses:
|
||||
- Expenses:Entertainment
|
||||
- Expenses:Gift
|
||||
- Expenses:Insurance
|
||||
- Expenses:Misc
|
||||
- Expenses:Shopping
|
||||
- Expenses:Utilities
|
||||
```
|
||||
|
||||
Now that the target is specified, you need to specify the list of
|
||||
accounts where you keep your retirement savings.
|
||||
|
||||
```yaml
|
||||
retirement:
|
||||
swr: 2
|
||||
expenses:
|
||||
- Expenses:Entertainment
|
||||
- Expenses:Gift
|
||||
- Expenses:Insurance
|
||||
- Expenses:Misc
|
||||
- Expenses:Shopping
|
||||
- Expenses:Utilities
|
||||
savings:
|
||||
- Assets:Equity:*
|
||||
- Assets:Debt:*
|
||||
```
|
|
@ -1,9 +1,8 @@
|
|||
|
||||
@font-face {
|
||||
font-family: "arcticons";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src: url("../fonts/arcticons.woff2") format("woff2");
|
||||
unicode-range: U+F0001-F1D3D;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
font-weight: 900;
|
||||
src: url("../fonts/fa6-brands.woff2") format("woff2");
|
||||
unicode-range: U+F2356-F2544;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
font-weight: 900;
|
||||
src: url("../fonts/fa6-regular.woff2") format("woff2");
|
||||
unicode-range: U+F22B2-F2355;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
font-weight: 900;
|
||||
src: url("../fonts/fa6-solid.woff2") format("woff2");
|
||||
unicode-range: U+F1D3E-F22B1;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
font-weight: 900;
|
||||
src: url("../fonts/fluent-emoji-high-contrast.woff2") format("woff2");
|
||||
unicode-range: U+F5B53-F615C;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
font-weight: 900;
|
||||
src: url("../fonts/mdi.woff2") format("woff2");
|
||||
unicode-range: U+F2545-F5B52;
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ async function createFont(font) {
|
|||
font-weight: 900;
|
||||
src: url("../fonts/${font}.woff2") format("woff2");
|
||||
unicode-range: U+${min.toString(16).toUpperCase()}-${max.toString(16).toUpperCase()};
|
||||
font-display: swap;
|
||||
font-display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
@ -83,7 +83,6 @@
|
|||
"minimum": 1,
|
||||
"maximum": 10
|
||||
},
|
||||
|
||||
"yearly_expenses": {
|
||||
"type": "integer",
|
||||
"description": "By default, average of last 3 year expenses will be used to calculate your yearly expenses. This can be overriden by setting this configuration to positive value"
|
||||
|
@ -108,7 +107,7 @@
|
|||
}
|
||||
},
|
||||
"ui:header": "name",
|
||||
"required": ["name", "swr", "savings"],
|
||||
"required": ["name", "icon", "swr", "savings"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
|
@ -150,7 +149,7 @@
|
|||
}
|
||||
},
|
||||
"ui:header": "name",
|
||||
"required": ["name", "target", "accounts"],
|
||||
"required": ["name", "icon", "target", "accounts"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,18 +69,27 @@ journal_path: '%s'
|
|||
db_path: '%s'
|
||||
ledger_cli: ledger
|
||||
default_currency: INR
|
||||
retirement:
|
||||
swr: 3
|
||||
goals:
|
||||
retirement:
|
||||
- name: Early Retirement
|
||||
icon: mdi:palm-tree
|
||||
swr: 3
|
||||
savings:
|
||||
- Assets:Debt:*
|
||||
- Assets:Equity:*
|
||||
expenses:
|
||||
- Expenses:Rent
|
||||
- Expenses:Utilities
|
||||
- Expenses:Shopping
|
||||
- Expenses:Restaurants
|
||||
- Expenses:Food
|
||||
- Expenses:Interest:*
|
||||
savings:
|
||||
- Assets:Debt:*
|
||||
- Assets:Equity:*
|
||||
expenses:
|
||||
- Expenses:Rent
|
||||
- Expenses:Utilities
|
||||
- Expenses:Shopping
|
||||
- Expenses:Restaurants
|
||||
- Expenses:Food
|
||||
- Expenses:Interest:*
|
||||
- name: Millionaire
|
||||
icon: mdi:car-sports
|
||||
target: 80000000
|
||||
accounts:
|
||||
- '!Assets:Checking'
|
||||
allocation_targets:
|
||||
- name: Debt
|
||||
target: 40
|
||||
|
|
|
@ -53,5 +53,14 @@ func getRetirementDetail(db *gorm.DB, conf config.RetirementGoal) gin.H {
|
|||
yearlyExpenses = calculateAverageExpense(db, conf)
|
||||
}
|
||||
|
||||
return gin.H{"savings_timeline": accounting.RunningBalance(db, savings), "savings_total": savingsTotal, "swr": conf.SWR, "yearly_expense": yearlyExpenses, "xirr": service.XIRR(db, savingsWithCapitalGains)}
|
||||
return gin.H{
|
||||
"type": "retirement",
|
||||
"name": conf.Name,
|
||||
"icon": conf.Icon,
|
||||
"savingsTimeline": accounting.RunningBalance(db, savings),
|
||||
"savingsTotal": savingsTotal,
|
||||
"swr": conf.SWR,
|
||||
"yearlyExpense": yearlyExpenses,
|
||||
"xirr": service.XIRR(db, savingsWithCapitalGains),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,10 +33,13 @@ func getSavingsDetail(db *gorm.DB, conf config.SavingsGoal) gin.H {
|
|||
savingsWithCapitalGains = service.PopulateMarketPrice(db, savingsWithCapitalGains)
|
||||
|
||||
return gin.H{
|
||||
"savings_timeline": accounting.RunningBalance(db, savings),
|
||||
"savings_total": savingsTotal,
|
||||
"target": decimal.NewFromFloat(conf.Target),
|
||||
"xirr": service.XIRR(db, savingsWithCapitalGains),
|
||||
"postings": savingsWithCapitalGains,
|
||||
"type": "savings",
|
||||
"name": conf.Name,
|
||||
"icon": conf.Icon,
|
||||
"savingsTimeline": accounting.RunningBalance(db, savings),
|
||||
"savingsTotal": savingsTotal,
|
||||
"target": decimal.NewFromFloat(conf.Target),
|
||||
"xirr": service.XIRR(db, savingsWithCapitalGains),
|
||||
"postings": savingsWithCapitalGains,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,12 @@ nav:
|
|||
- reference/budget.md
|
||||
- reference/bulk-edit.md
|
||||
- reference/import.md
|
||||
- reference/retirement.md
|
||||
- reference/recurring.md
|
||||
- reference/config.md
|
||||
- 'Goals':
|
||||
- reference/goals/index.md
|
||||
- reference/goals/retirement.md
|
||||
- reference/goals/savings.md
|
||||
- reference/ledger-cli.md
|
||||
- reference/analysis.md
|
||||
- 'Tax':
|
||||
|
|
|
@ -119,7 +119,7 @@
|
|||
href: "/more",
|
||||
children: [
|
||||
{ label: "Configuration", href: "/config", tag: "alpha" },
|
||||
{ label: "Goals", href: "/goals" },
|
||||
{ label: "Goals", href: "/goals", help: "goals" },
|
||||
{ label: "Doctor", href: "/doctor" },
|
||||
{ label: "Logs", href: "/logs" }
|
||||
]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { formatCurrency, type Posting } from "$lib/utils";
|
||||
import _ from "lodash";
|
||||
|
||||
export let postings: Posting[];
|
||||
export let groupFormat: string;
|
||||
|
@ -12,6 +13,7 @@
|
|||
|
||||
let groupedPostings: GroupedPosting[] = [];
|
||||
$: groupedPostings = group(postings);
|
||||
$: isGrouped = _.some(groupedPostings, (groupedPosting) => groupedPosting.postings.length > 1);
|
||||
|
||||
function group(ps: Posting[]) {
|
||||
let groupedPostings: GroupedPosting[] = [];
|
||||
|
@ -45,11 +47,13 @@
|
|||
|
||||
<div>
|
||||
{#each groupedPostings as groupedPosting}
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between -mb-1 has-text-weight-bold has-text-grey-light">
|
||||
<div>{groupedPosting.key}</div>
|
||||
<div>{formatCurrency(groupedPosting.total)}</div>
|
||||
</div>
|
||||
<div class={isGrouped && "mb-3"}>
|
||||
{#if isGrouped}
|
||||
<div class="flex justify-between -mb-1 has-text-weight-bold has-text-grey-light">
|
||||
<div>{groupedPosting.key}</div>
|
||||
<div>{formatCurrency(groupedPosting.total)}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<slot groupedPostings={groupedPosting.postings} />
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
@ -330,19 +330,25 @@ export interface AccountBudget {
|
|||
}
|
||||
|
||||
export interface RetirementGoalProgress {
|
||||
savings_total: number;
|
||||
savings_timeline: Point[];
|
||||
savingsTotal: number;
|
||||
savingsTimeline: Point[];
|
||||
swr: number;
|
||||
yearly_expense: number;
|
||||
yearlyExpense: number;
|
||||
xirr: number;
|
||||
name: string;
|
||||
type: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface SavingsGoalProgress {
|
||||
savings_total: number;
|
||||
savings_timeline: Point[];
|
||||
savingsTotal: number;
|
||||
savingsTimeline: Point[];
|
||||
target: number;
|
||||
xirr: number;
|
||||
postings: Posting[];
|
||||
name: string;
|
||||
type: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface LedgerFile {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { ajax, formatCurrency, formatFloat, type Networth } from "$lib/utils";
|
||||
import { ajax, formatCurrency, formatFloat, isMobile, type Networth } from "$lib/utils";
|
||||
import COLORS from "$lib/colors";
|
||||
import { renderNetworth } from "$lib/networth";
|
||||
import _ from "lodash";
|
||||
|
@ -50,8 +50,8 @@
|
|||
</script>
|
||||
|
||||
<section class="section tab-networth">
|
||||
<div class="container">
|
||||
<nav class="level">
|
||||
<div class="container is-fluid">
|
||||
<nav class="level {isMobile() && 'grid-2'}">
|
||||
<LevelItem title="Net worth" color={COLORS.primary} value={formatCurrency(networth)} />
|
||||
<LevelItem
|
||||
title="Net Investment"
|
||||
|
|
|
@ -2,16 +2,20 @@
|
|||
import COLORS from "$lib/colors";
|
||||
import LevelItem from "$lib/components/LevelItem.svelte";
|
||||
import Progress from "$lib/components/Progress.svelte";
|
||||
import ZeroState from "$lib/components/ZeroState.svelte";
|
||||
import { iconGlyph } from "$lib/icon";
|
||||
import { ajax, formatCurrency, type GoalSummary } from "$lib/utils";
|
||||
import { ajax, formatCurrency, helpUrl, type GoalSummary } from "$lib/utils";
|
||||
import _ from "lodash";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let isEmpty = false;
|
||||
let goals: GoalSummary[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
({ goals } = await ajax("/api/goals"));
|
||||
console.log(goals);
|
||||
if (_.isEmpty(goals)) {
|
||||
isEmpty = true;
|
||||
}
|
||||
});
|
||||
|
||||
function percentComplete(goal: GoalSummary) {
|
||||
|
@ -22,8 +26,15 @@
|
|||
<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>
|
||||
|
||||
{#each goals as goal}
|
||||
<div class="column is-one-third-widescreen is-half-desktop">
|
||||
<div class="column is-6 is-one-third-widescreen">
|
||||
<div class="box p-3">
|
||||
<div class="flex justify-between mb-4">
|
||||
<a
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<script lang="ts">
|
||||
import COLORS from "$lib/colors";
|
||||
import Progress from "$lib/components/Progress.svelte";
|
||||
import { ajax, formatCurrency, formatFloat, type Point } from "$lib/utils";
|
||||
import { ajax, formatCurrency, formatFloat, isMobile, type Point } from "$lib/utils";
|
||||
import { onMount, tick, onDestroy } from "svelte";
|
||||
import ARIMAPromise from "arima/async";
|
||||
import { forecast, renderProgress, findBreakPoints } from "$lib/goals";
|
||||
import LevelItem from "$lib/components/LevelItem.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import { iconGlyph } from "$lib/icon";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let svg: Element;
|
||||
let savingsTotal = 0,
|
||||
icon = "",
|
||||
name = "",
|
||||
targetSavings = 0,
|
||||
swr = 0,
|
||||
xirr = 0,
|
||||
|
@ -28,13 +31,11 @@
|
|||
});
|
||||
|
||||
onMount(async () => {
|
||||
({
|
||||
savings_total: savingsTotal,
|
||||
savings_timeline: savingsTimeline,
|
||||
yearly_expense: yearlyExpense,
|
||||
swr,
|
||||
xirr
|
||||
} = await ajax("/api/goals/retirement/:name", null, data));
|
||||
({ savingsTotal, savingsTimeline, yearlyExpense, swr, xirr, icon, name } = await ajax(
|
||||
"/api/goals/retirement/:name",
|
||||
null,
|
||||
data
|
||||
));
|
||||
targetSavings = yearlyExpense * (100 / swr);
|
||||
|
||||
if (yearlyExpense > 0) {
|
||||
|
@ -59,7 +60,8 @@
|
|||
|
||||
<section class="section">
|
||||
<div class="container is-fluid">
|
||||
<nav class="level">
|
||||
<nav class="level custom-icon {isMobile() && 'grid-2'}">
|
||||
<LevelItem title={name} value={iconGlyph(icon)} />
|
||||
<LevelItem
|
||||
title="Current Savings"
|
||||
value={formatCurrency(savingsTotal)}
|
||||
|
@ -100,9 +102,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column is-12 has-text-centered">
|
||||
<div class="column is-12 has-text-centered has-text-grey">
|
||||
<div>
|
||||
<p class="heading">Retirement Progress</p>
|
||||
<p class="is-size-5 custom-icon">{iconGlyph(icon)} {name} progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
<script lang="ts">
|
||||
import COLORS from "$lib/colors";
|
||||
import Progress from "$lib/components/Progress.svelte";
|
||||
import { ajax, formatCurrency, formatFloat, type Point, type Posting } from "$lib/utils";
|
||||
import {
|
||||
ajax,
|
||||
formatCurrency,
|
||||
formatFloat,
|
||||
isMobile,
|
||||
type Point,
|
||||
type Posting
|
||||
} from "$lib/utils";
|
||||
import { onMount, tick, onDestroy } from "svelte";
|
||||
import ARIMAPromise from "arima/async";
|
||||
import { forecast, renderProgress, findBreakPoints } from "$lib/goals";
|
||||
|
@ -10,6 +17,7 @@
|
|||
import type { PageData } from "./$types";
|
||||
import PostingCard from "$lib/components/PostingCard.svelte";
|
||||
import PostingGroup from "$lib/components/PostingGroup.svelte";
|
||||
import { iconGlyph } from "$lib/icon";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
|
@ -17,6 +25,8 @@
|
|||
let savingsTotal = 0,
|
||||
targetSavings = 0,
|
||||
xirr = 0,
|
||||
name = "",
|
||||
icon = "",
|
||||
progressPercent = 0,
|
||||
breakPoints: Point[] = [],
|
||||
savingsTimeline: Point[] = [],
|
||||
|
@ -29,10 +39,12 @@
|
|||
|
||||
onMount(async () => {
|
||||
({
|
||||
savings_total: savingsTotal,
|
||||
savings_timeline: savingsTimeline,
|
||||
savingsTotal,
|
||||
savingsTimeline,
|
||||
target: targetSavings,
|
||||
postings,
|
||||
icon,
|
||||
name,
|
||||
xirr
|
||||
} = await ajax("/api/goals/savings/:name", null, data));
|
||||
|
||||
|
@ -56,7 +68,8 @@
|
|||
|
||||
<section class="section">
|
||||
<div class="container is-fluid">
|
||||
<nav class="level">
|
||||
<nav class="level custom-icon {isMobile() && 'grid-2'}">
|
||||
<LevelItem title={name} value={iconGlyph(icon)} />
|
||||
<LevelItem
|
||||
title="Current Savings"
|
||||
value={formatCurrency(savingsTotal)}
|
||||
|
@ -108,9 +121,9 @@
|
|||
<svg height="500" bind:this={svg} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12 has-text-centered">
|
||||
<div class="column is-12 has-text-centered has-text-grey">
|
||||
<div>
|
||||
<p class="heading">Savings Progress</p>
|
||||
<p class="is-size-5 custom-icon">{iconGlyph(icon)} {name} progress</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue