Everything you need to build the modular ANSI C amortization calculator with SQLite3 member storage. Copy each file or use the setup script to create them all at once.
You need a C compiler and the SQLite3 development library.
macOS ships with SQLite3 built in — no extra install needed.
Create the project folder and the src/ subdirectory:
The final structure looks like this:
Click any file to expand and view its contents. Use the Copy button to copy a single file, or use the setup script below to create them all at once.
# Amortization Calculator - Makefile
# Works with both GNU make (Linux) and BSD make (macOS)
CC = gcc
CFLAGS = -Wall -Wextra -Isrc
LDFLAGS = -lm -lsqlite3
BIN = amortization
OBJS = build/main.o build/util.o build/calc.o build/db.o \
build/dashboard.o build/schedule.o build/graphs.o build/members.o
.PHONY: all clean
all: build $(BIN)
build:
mkdir -p build
$(BIN): $(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS) $(LDFLAGS)
@echo ""
@echo " Build complete: ./$(BIN)"
@echo ""
build/main.o: src/main.c
$(CC) $(CFLAGS) -c -o $@ src/main.c
build/util.o: src/util.c
$(CC) $(CFLAGS) -c -o $@ src/util.c
build/calc.o: src/calc.c
$(CC) $(CFLAGS) -c -o $@ src/calc.c
build/db.o: src/db.c
$(CC) $(CFLAGS) -c -o $@ src/db.c
build/dashboard.o: src/dashboard.c
$(CC) $(CFLAGS) -c -o $@ src/dashboard.c
build/schedule.o: src/schedule.c
$(CC) $(CFLAGS) -c -o $@ src/schedule.c
build/graphs.o: src/graphs.c
$(CC) $(CFLAGS) -c -o $@ src/graphs.c
build/members.o: src/members.c
$(CC) $(CFLAGS) -c -o $@ src/members.c
clean:
rm -rf build $(BIN)
@echo " Cleaned."
/*
* types.h - Shared data structures, constants, and ANSI color codes
*/
#ifndef TYPES_H
#define TYPES_H
#define MAX_PAYMENTS 600
#define MAX_NAME 64
#define W 90
#define DB_FILE "loan_members.db"
/* ── ANSI Color Codes ── */
#define C_RESET "\033[0m"
#define C_BOLD "\033[1m"
#define C_DIM "\033[2m"
#define C_RED "\033[31m"
#define C_GREEN "\033[32m"
#define C_YELLOW "\033[33m"
#define C_BLUE "\033[34m"
#define C_MAGENTA "\033[35m"
#define C_CYAN "\033[36m"
#define C_WHITE "\033[37m"
#define CB_RED "\033[1;31m"
#define CB_GREEN "\033[1;32m"
#define CB_YELLOW "\033[1;33m"
#define CB_BLUE "\033[1;34m"
#define CB_MAGENTA "\033[1;35m"
#define CB_CYAN "\033[1;36m"
#define CB_WHITE "\033[1;37m"
#define BG_BLUE "\033[44m"
#define BG_CYAN "\033[46m"
#define C_HEADER "\033[1;37;44m"
#define C_MENU_KEY "\033[1;33m"
#define C_MENU_TXT "\033[36m"
#define C_LABEL "\033[1;36m"
#define C_VALUE "\033[1;37m"
#define C_MONEY "\033[1;32m"
#define C_RATE "\033[1;33m"
#define C_TBL_HDR "\033[1;37;46m"
#define C_SEPARATOR "\033[34m"
#define C_BAR_PRINC "\033[1;32m"
#define C_BAR_INT "\033[1;31m"
#define C_ERROR "\033[1;31m"
#define C_PROMPT "\033[1;33m"
#define C_YEAR_MARK "\033[1;35m"
#define C_BOX "\033[1;34m"
#define C_BOX_TEXT "\033[1;37m"
/* ── Data Structures ── */
typedef struct {
int number;
double payment;
double principal;
double interest;
double balance;
double cumulative_interest;
double cumulative_principal;
} PaymentRow;
typedef struct {
int member_id; /* 0 = unsaved / manual entry */
char member_name[MAX_NAME];
double loan_amount;
double annual_rate;
int term_months;
double monthly_payment;
double total_paid;
double total_interest;
int num_payments;
PaymentRow schedule[MAX_PAYMENTS];
} AmortTable;
#endif /* TYPES_H */
/*
* util.h - Screen, box drawing, formatting, input utilities
*/
#ifndef UTIL_H
#define UTIL_H
void clear_screen(void);
void color_line(const char *color, char ch, int width);
void print_box_top(int width);
void print_box_bottom(int width);
void print_box_sep(int width);
void print_box_line(const char *text, const char *text_color, int width);
void print_box_empty(int width);
void format_currency(char *buf, double value);
void wait_for_enter(void);
double get_double(const char *prompt);
int get_int(const char *prompt);
void get_string(const char *prompt, char *buf, int size);
#endif
/*
* util.c - Screen, box drawing, formatting, input utilities
*/
#include <stdio.h>
#include <string.h>
#include "types.h"
#include "util.h"
void clear_screen(void)
{
printf("\033[2J\033[H");
fflush(stdout);
}
void color_line(const char *color, char ch, int width)
{
int i;
printf("%s", color);
for (i = 0; i < width; i++) putchar(ch);
printf("%s\n", C_RESET);
}
void print_box_top(int width)
{
int i;
printf("%s+", C_BOX);
for (i = 0; i < width - 2; i++) putchar('=');
printf("+%s\n", C_RESET);
}
void print_box_bottom(int width)
{
print_box_top(width);
}
void print_box_sep(int width)
{
int i;
printf("%s||", C_BOX);
printf("%s", C_DIM);
for (i = 0; i < width - 4; i++) putchar('-');
printf("%s%s||%s\n", C_RESET, C_BOX, C_RESET);
}
void print_box_line(const char *text, const char *text_color, int width)
{
int len = (int)strlen(text);
int pad = width - 4 - len;
int left, right, i;
if (pad < 0) pad = 0;
left = pad / 2;
right = pad - left;
printf("%s||%s", C_BOX, text_color);
for (i = 0; i < left; i++) putchar(' ');
printf("%s", text);
for (i = 0; i < right; i++) putchar(' ');
printf("%s%s||%s\n", C_RESET, C_BOX, C_RESET);
}
void print_box_empty(int width)
{
print_box_line("", C_RESET, width);
}
void format_currency(char *buf, double value)
{
int whole, cents, millions, thousands, remainder;
char sign;
if (value < 0.0) {
sign = '-';
value = -value;
} else {
sign = ' ';
}
whole = (int)value;
cents = (int)((value - whole) * 100.0 + 0.5);
if (cents >= 100) { whole++; cents -= 100; }
millions = whole / 1000000;
thousands = (whole % 1000000) / 1000;
remainder = whole % 1000;
if (millions > 0)
sprintf(buf, "%c$%d,%03d,%03d.%02d", sign, millions, thousands, remainder, cents);
else if (thousands > 0)
sprintf(buf, "%c$%d,%03d.%02d", sign, thousands, remainder, cents);
else
sprintf(buf, "%c$%d.%02d", sign, remainder, cents);
}
void wait_for_enter(void)
{
char line[128];
printf("\n %sPress [Enter] to continue...%s", C_DIM, C_RESET);
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL) return;
}
double get_double(const char *prompt)
{
char line[128];
double val;
printf(" %s%s%s ", C_PROMPT, prompt, C_RESET);
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL) return -1.0;
if (sscanf(line, "%lf", &val) != 1) return -1.0;
return val;
}
int get_int(const char *prompt)
{
char line[128];
int val;
printf(" %s%s%s ", C_PROMPT, prompt, C_RESET);
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL) return -1;
if (sscanf(line, "%d", &val) != 1) return -1;
return val;
}
void get_string(const char *prompt, char *buf, int size)
{
int len;
printf(" %s%s%s ", C_PROMPT, prompt, C_RESET);
fflush(stdout);
if (fgets(buf, size, stdin) == NULL) {
buf[0] = '\0';
return;
}
len = (int)strlen(buf);
if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0';
}
/*
* calc.h - Amortization calculation engine
*/
#ifndef CALC_H
#define CALC_H
#include "types.h"
double calc_monthly_payment(double principal, double monthly_rate, int n);
int generate_schedule(AmortTable *tbl);
#endif
/*
* calc.c - Amortization calculation engine
*/
#include <math.h>
#include "types.h"
#include "calc.h"
double calc_monthly_payment(double principal, double monthly_rate, int n)
{
double factor;
if (monthly_rate == 0.0) return principal / n;
factor = pow(1.0 + monthly_rate, (double)n);
return principal * (monthly_rate * factor) / (factor - 1.0);
}
int generate_schedule(AmortTable *tbl)
{
double monthly_rate, balance, cum_int, cum_princ;
int i;
if (tbl->loan_amount <= 0.0 || tbl->term_months <= 0 ||
tbl->term_months > MAX_PAYMENTS || tbl->annual_rate < 0.0)
return -1;
monthly_rate = tbl->annual_rate / 100.0 / 12.0;
tbl->monthly_payment = calc_monthly_payment(
tbl->loan_amount, monthly_rate, tbl->term_months);
balance = tbl->loan_amount;
cum_int = 0.0;
cum_princ = 0.0;
for (i = 0; i < tbl->term_months; i++) {
PaymentRow *r = &tbl->schedule[i];
double int_part = balance * monthly_rate;
double princ_part;
double pmt = tbl->monthly_payment;
if (i == tbl->term_months - 1) pmt = balance + int_part;
princ_part = pmt - int_part;
balance -= princ_part;
cum_int += int_part;
cum_princ += princ_part;
r->number = i + 1;
r->payment = pmt;
r->principal = princ_part;
r->interest = int_part;
r->balance = (balance < 0.01) ? 0.0 : balance;
r->cumulative_interest = cum_int;
r->cumulative_principal = cum_princ;
}
tbl->num_payments = tbl->term_months;
tbl->total_paid = cum_princ + cum_int;
tbl->total_interest = cum_int;
return 0;
}
/*
* db.h - SQLite3 member database operations
*/
#ifndef DB_H
#define DB_H
#include "types.h"
int db_init(void);
void db_close(void);
int db_save_member(AmortTable *tbl);
int db_load_member(int id, AmortTable *tbl);
int db_delete_member(int id);
int db_update_member(const AmortTable *tbl);
int db_list_members(void);
int db_count_members(void);
#endif
/*
* db.c - SQLite3 member database operations
*/
#include <stdio.h>
#include <string.h>
#include <sqlite3.h>
#include "types.h"
#include "util.h"
#include "db.h"
static sqlite3 *g_db = NULL;
int db_init(void)
{
const char *sql =
"CREATE TABLE IF NOT EXISTS members ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL,"
" loan_amount REAL NOT NULL,"
" annual_rate REAL NOT NULL,"
" term_months INTEGER NOT NULL,"
" created_at TEXT DEFAULT (datetime('now','localtime'))"
");";
char *err = NULL;
int rc;
rc = sqlite3_open(DB_FILE, &g_db);
if (rc != SQLITE_OK) {
printf(" %sDatabase error: %s%s\n", C_ERROR, sqlite3_errmsg(g_db), C_RESET);
return -1;
}
rc = sqlite3_exec(g_db, sql, NULL, NULL, &err);
if (rc != SQLITE_OK) {
printf(" %sSQL error: %s%s\n", C_ERROR, err, C_RESET);
sqlite3_free(err);
return -1;
}
return 0;
}
void db_close(void)
{
if (g_db) {
sqlite3_close(g_db);
g_db = NULL;
}
}
int db_save_member(AmortTable *tbl)
{
const char *sql =
"INSERT INTO members (name, loan_amount, annual_rate, term_months) "
"VALUES (?, ?, ?, ?);";
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, tbl->member_name, -1, SQLITE_STATIC);
sqlite3_bind_double(stmt, 2, tbl->loan_amount);
sqlite3_bind_double(stmt, 3, tbl->annual_rate);
sqlite3_bind_int(stmt, 4, tbl->term_months);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) return -1;
tbl->member_id = (int)sqlite3_last_insert_rowid(g_db);
return 0;
}
int db_load_member(int id, AmortTable *tbl)
{
const char *sql =
"SELECT id, name, loan_amount, annual_rate, term_months "
"FROM members WHERE id = ?;";
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return -1;
sqlite3_bind_int(stmt, 1, id);
rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return -1;
}
memset(tbl, 0, sizeof(*tbl));
tbl->member_id = sqlite3_column_int(stmt, 0);
strncpy(tbl->member_name, (const char *)sqlite3_column_text(stmt, 1), MAX_NAME - 1);
tbl->member_name[MAX_NAME - 1] = '\0';
tbl->loan_amount = sqlite3_column_double(stmt, 2);
tbl->annual_rate = sqlite3_column_double(stmt, 3);
tbl->term_months = sqlite3_column_int(stmt, 4);
sqlite3_finalize(stmt);
return 0;
}
int db_delete_member(int id)
{
const char *sql = "DELETE FROM members WHERE id = ?;";
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return -1;
sqlite3_bind_int(stmt, 1, id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return (rc == SQLITE_DONE && sqlite3_changes(g_db) > 0) ? 0 : -1;
}
int db_update_member(const AmortTable *tbl)
{
const char *sql =
"UPDATE members SET name=?, loan_amount=?, annual_rate=?, term_months=? "
"WHERE id=?;";
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return -1;
sqlite3_bind_text(stmt, 1, tbl->member_name, -1, SQLITE_STATIC);
sqlite3_bind_double(stmt, 2, tbl->loan_amount);
sqlite3_bind_double(stmt, 3, tbl->annual_rate);
sqlite3_bind_int(stmt, 4, tbl->term_months);
sqlite3_bind_int(stmt, 5, tbl->member_id);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return (rc == SQLITE_DONE) ? 0 : -1;
}
int db_list_members(void)
{
const char *sql =
"SELECT id, name, loan_amount, annual_rate, term_months, created_at "
"FROM members ORDER BY name;";
sqlite3_stmt *stmt;
int rc, count = 0;
char amt[32];
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) return -1;
printf(" %s %-4s %-20s %14s %8s %6s %-16s %s\n",
C_TBL_HDR, "ID", "Name", "Loan Amount", "Rate",
"Months", "Added", C_RESET);
color_line(C_SEPARATOR, '-', 82);
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *row_clr = (count % 2 == 0) ? C_WHITE : C_DIM;
format_currency(amt, sqlite3_column_double(stmt, 2));
printf(" %s%-4d%s %s%-20.20s%s %s%14s%s %s%7.3f%%%s %s%6d%s %s%-16.16s%s\n",
CB_CYAN, sqlite3_column_int(stmt, 0), C_RESET,
row_clr, sqlite3_column_text(stmt, 1), C_RESET,
C_MONEY, amt, C_RESET,
C_RATE, sqlite3_column_double(stmt, 3), C_RESET,
row_clr, sqlite3_column_int(stmt, 4), C_RESET,
C_DIM, sqlite3_column_text(stmt, 5), C_RESET);
count++;
}
sqlite3_finalize(stmt);
if (count == 0) {
printf(" %sNo members found.%s\n", C_DIM, C_RESET);
}
color_line(C_SEPARATOR, '-', 82);
printf(" %s%d member(s)%s\n\n", C_DIM, count, C_RESET);
return count;
}
int db_count_members(void)
{
const char *sql = "SELECT COUNT(*) FROM members;";
sqlite3_stmt *stmt;
int count = 0;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW)
count = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
return count;
}
/*
* dashboard.h - Dashboard screen
*/
#ifndef DASHBOARD_H
#define DASHBOARD_H
#include "types.h"
void print_dashboard(const AmortTable *tbl);
#endif
/*
* dashboard.c - Dashboard screen (summary + annual breakdown)
*/
#include <stdio.h>
#include "types.h"
#include "util.h"
#include "dashboard.h"
void print_dashboard(const AmortTable *tbl)
{
char b1[32], b2[32], b3[32], b4[32];
int total_years, year;
double int_pct;
clear_screen();
/* Title */
printf("\n");
print_box_top(W);
print_box_empty(W);
if (tbl->member_id > 0) {
char title[128];
sprintf(title, "D A S H B O A R D - %.60s", tbl->member_name);
print_box_line(title, CB_CYAN, W);
} else {
print_box_line("D A S H B O A R D", CB_CYAN, W);
}
print_box_empty(W);
print_box_bottom(W);
printf("\n");
/* Loan Summary */
format_currency(b1, tbl->loan_amount);
format_currency(b2, tbl->monthly_payment);
format_currency(b3, tbl->total_paid);
format_currency(b4, tbl->total_interest);
int_pct = (tbl->total_paid > 0.0)
? (tbl->total_interest / tbl->total_paid * 100.0) : 0.0;
color_line(C_SEPARATOR, '=', W);
printf(" %s LOAN SUMMARY%s\n", CB_WHITE, C_RESET);
color_line(C_SEPARATOR, '-', W);
printf(" %sLoan Amount:%s %s%-20s%s", C_LABEL, C_RESET, C_MONEY, b1, C_RESET);
printf(" %sMonthly Payment:%s %s%s%s\n", C_LABEL, C_RESET, C_MONEY, b2, C_RESET);
printf(" %sAnnual Rate:%s %s%-19.3f%%%s", C_LABEL, C_RESET, C_RATE, tbl->annual_rate, C_RESET);
printf(" %sTerm:%s %s%d mo (%dy %dm)%s\n",
C_LABEL, C_RESET, C_VALUE,
tbl->term_months, tbl->term_months / 12, tbl->term_months % 12, C_RESET);
printf(" %sTotal Paid:%s %s%-20s%s", C_LABEL, C_RESET, C_MONEY, b3, C_RESET);
printf(" %sTotal Interest:%s %s%s%s (%s%.1f%%%s)\n",
C_LABEL, C_RESET, CB_RED, b4, C_RESET, C_RATE, int_pct, C_RESET);
color_line(C_SEPARATOR, '=', W);
printf("\n");
/* Annual Breakdown */
total_years = (tbl->term_months + 11) / 12;
printf(" %s ANNUAL BREAKDOWN%s\n", CB_WHITE, C_RESET);
color_line(C_SEPARATOR, '-', 78);
printf(" %s %-6s %14s %14s %16s %14s %s\n",
C_TBL_HDR, "Year", "Principal", "Interest",
"End Balance", "Paid Off", C_RESET);
color_line(C_SEPARATOR, '-', 78);
for (year = 0; year < total_years; year++) {
int start = year * 12;
int end = start + 12;
double yp = 0.0, yi = 0.0, yb = 0.0, pct;
char sp[20], si[20], sb[20];
int m;
const char *row_clr;
if (end > tbl->num_payments) end = tbl->num_payments;
for (m = start; m < end; m++) {
yp += tbl->schedule[m].principal;
yi += tbl->schedule[m].interest;
yb = tbl->schedule[m].balance;
}
pct = (tbl->loan_amount > 0.0)
? ((tbl->loan_amount - yb) / tbl->loan_amount * 100.0) : 100.0;
format_currency(sp, yp);
format_currency(si, yi);
format_currency(sb, yb);
row_clr = (year % 2 == 0) ? C_WHITE : C_DIM;
printf(" %s%-6d%s %s%14s%s %s%14s%s %s%16s%s",
CB_CYAN, year + 1, C_RESET,
C_GREEN, sp, C_RESET,
C_RED, si, C_RESET,
row_clr, sb, C_RESET);
if (pct >= 75.0)
printf(" %s%12.1f%%%s\n", CB_GREEN, pct, C_RESET);
else if (pct >= 40.0)
printf(" %s%12.1f%%%s\n", CB_YELLOW, pct, C_RESET);
else
printf(" %s%12.1f%%%s\n", C_WHITE, pct, C_RESET);
}
color_line(C_SEPARATOR, '-', 78);
printf("\n");
}
/*
* schedule.h - Paginated monthly schedule screen
*/
#ifndef SCHEDULE_H
#define SCHEDULE_H
#include "types.h"
void print_monthly_schedule(const AmortTable *tbl);
#endif
/*
* schedule.c - Paginated monthly schedule screen
*/
#include <stdio.h>
#include "types.h"
#include "util.h"
#include "schedule.h"
void print_monthly_schedule(const AmortTable *tbl)
{
int i;
int page_size = 24;
int page = 0;
int total_pages;
char pay[20], princ[20], intr[20], bal[20], cum_i[20];
char line[128];
total_pages = (tbl->num_payments + page_size - 1) / page_size;
while (page < total_pages) {
int start = page * page_size;
int end = start + page_size;
if (end > tbl->num_payments) end = tbl->num_payments;
clear_screen();
printf("\n");
print_box_top(W);
print_box_line("M O N T H L Y S C H E D U L E", CB_CYAN, W);
{
char pg[64];
sprintf(pg, "Page %d of %d", page + 1, total_pages);
print_box_line(pg, C_DIM, W);
}
print_box_bottom(W);
printf("\n");
printf(" %s %-6s %12s %12s %12s %14s %14s %s\n",
C_TBL_HDR,
"Pay #", "Payment", "Principal", "Interest",
"Balance", "Cum. Interest", C_RESET);
color_line(C_SEPARATOR, '-', W);
for (i = start; i < end; i++) {
const PaymentRow *r = &tbl->schedule[i];
const char *row_clr;
format_currency(pay, r->payment);
format_currency(princ, r->principal);
format_currency(intr, r->interest);
format_currency(bal, r->balance);
format_currency(cum_i, r->cumulative_interest);
row_clr = ((i - start) % 2 == 0) ? C_WHITE : C_DIM;
printf(" %s%-6d%s %s%12s%s %s%12s%s %s%12s%s %s%14s%s %s%14s%s\n",
CB_CYAN, r->number, C_RESET,
row_clr, pay, C_RESET,
C_GREEN, princ, C_RESET,
C_RED, intr, C_RESET,
row_clr, bal, C_RESET,
C_YELLOW, cum_i, C_RESET);
if (r->number % 12 == 0 && i < tbl->num_payments - 1) {
printf(" %s---- Year %d complete | Balance: %s %s",
C_YEAR_MARK, r->number / 12, bal, C_RESET);
color_line(C_YEAR_MARK, '-', 30);
}
}
printf("\n %s[N]%s Next %s[P]%s Prev %s[Q]%s Back\n",
C_MENU_KEY, C_RESET, C_MENU_KEY, C_RESET, C_MENU_KEY, C_RESET);
printf(" %s>%s ", C_PROMPT, C_RESET);
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL) break;
if (line[0] == 'n' || line[0] == 'N') {
if (page < total_pages - 1) page++;
} else if (line[0] == 'p' || line[0] == 'P') {
if (page > 0) page--;
} else if (line[0] == 'q' || line[0] == 'Q') {
break;
}
}
}
/*
* graphs.h - Graph screens (bar chart, balance curve, payoff gauge)
*/
#ifndef GRAPHS_H
#define GRAPHS_H
#include "types.h"
void graphs_menu(const AmortTable *tbl);
#endif
/*
* graphs.c - Graph screens with submenu
* 1) Principal vs Interest bar chart (by year)
* 2) Balance curve (ASCII line graph)
* 3) Cumulative interest breakdown
*/
#include <stdio.h>
#include <string.h>
#include "types.h"
#include "util.h"
#include "graphs.h"
/* ── Helper: print a loan reference line ── */
static void print_loan_ref(const AmortTable *tbl)
{
char b1[32], b2[32], b3[32];
double int_pct;
format_currency(b1, tbl->loan_amount);
format_currency(b2, tbl->monthly_payment);
format_currency(b3, tbl->total_interest);
int_pct = (tbl->total_paid > 0.0)
? (tbl->total_interest / tbl->total_paid * 100.0) : 0.0;
if (tbl->member_id > 0)
printf(" %sMember:%s %s%s%s ", C_LABEL, C_RESET, CB_WHITE, tbl->member_name, C_RESET);
printf("%sLoan:%s %s%s%s %sRate:%s %s%.3f%%%s %sPmt:%s %s%s%s %sInt:%s %s%s%s (%s%.1f%%%s)\n",
C_LABEL, C_RESET, C_MONEY, b1, C_RESET,
C_LABEL, C_RESET, C_RATE, tbl->annual_rate, C_RESET,
C_LABEL, C_RESET, C_MONEY, b2, C_RESET,
C_LABEL, C_RESET, CB_RED, b3, C_RESET,
C_RATE, int_pct, C_RESET);
printf("\n");
}
/* ════════════════════════════════════════════════════════════
GRAPH 1: Principal vs Interest Bar Chart
════════════════════════════════════════════════════════════ */
static void graph_bar_chart(const AmortTable *tbl)
{
int total_years, year;
int bar_width = 40;
clear_screen();
printf("\n");
print_box_top(W);
print_box_empty(W);
print_box_line("P R I N C I P A L vs I N T E R E S T", CB_CYAN, W);
print_box_line("By Year", C_DIM, W);
print_box_empty(W);
print_box_bottom(W);
printf("\n");
print_loan_ref(tbl);
total_years = (tbl->term_months + 11) / 12;
printf(" %s PRINCIPAL vs INTEREST BY YEAR%s\n", CB_WHITE, C_RESET);
printf(" Legend: %s[####]%s Principal %s[....]%s Interest\n",
C_BAR_PRINC, C_RESET, C_BAR_INT, C_RESET);
color_line(C_SEPARATOR, '-', 64);
for (year = 0; year < total_years; year++) {
int start = year * 12;
int end = start + 12;
double yp = 0.0, yi = 0.0, total, ratio;
int pb, ib, b;
int m;
if (end > tbl->num_payments) end = tbl->num_payments;
for (m = start; m < end; m++) {
yp += tbl->schedule[m].principal;
yi += tbl->schedule[m].interest;
}
total = yp + yi;
ratio = (total > 0.0) ? yp / total : 0.0;
pb = (int)(ratio * bar_width + 0.5);
ib = bar_width - pb;
printf(" %sY%-2d%s %s|%s", CB_CYAN, year + 1, C_RESET, C_DIM, C_RESET);
printf("%s", C_BAR_PRINC);
for (b = 0; b < pb; b++) putchar('#');
printf("%s", C_BAR_INT);
for (b = 0; b < ib; b++) putchar('.');
printf("%s%s|%s", C_RESET, C_DIM, C_RESET);
printf(" %s%3.0f%%%s/%s%3.0f%%%s\n",
C_GREEN, ratio * 100.0, C_RESET,
C_RED, (1.0 - ratio) * 100.0, C_RESET);
}
color_line(C_SEPARATOR, '-', 64);
/* Totals */
{
char tp[20], ti[20];
double princ_pct, int_pct2;
format_currency(tp, tbl->loan_amount);
format_currency(ti, tbl->total_interest);
princ_pct = (tbl->total_paid > 0.0)
? (tbl->loan_amount / tbl->total_paid * 100.0) : 0.0;
int_pct2 = 100.0 - princ_pct;
printf(" %sTotals:%s Principal: %s%s%s (%s%.1f%%%s) "
"Interest: %s%s%s (%s%.1f%%%s)\n\n",
CB_WHITE, C_RESET,
C_MONEY, tp, C_RESET, C_GREEN, princ_pct, C_RESET,
CB_RED, ti, C_RESET, C_RED, int_pct2, C_RESET);
}
}
/* ════════════════════════════════════════════════════════════
GRAPH 2: Balance Curve (ASCII line graph)
════════════════════════════════════════════════════════════ */
static void graph_balance_curve(const AmortTable *tbl)
{
int total_years, year;
int graph_height = 20;
int graph_width;
double max_val;
int row, col;
char grid[22][62]; /* max 20 rows x 60 cols + padding */
clear_screen();
printf("\n");
print_box_top(W);
print_box_empty(W);
print_box_line("B A L A N C E C U R V E", CB_CYAN, W);
print_box_line("Remaining Balance Over Time", C_DIM, W);
print_box_empty(W);
print_box_bottom(W);
printf("\n");
print_loan_ref(tbl);
total_years = (tbl->term_months + 11) / 12;
graph_width = total_years;
if (graph_width > 60) graph_width = 60;
max_val = tbl->loan_amount;
/* Initialize grid */
for (row = 0; row < graph_height; row++)
for (col = 0; col < graph_width; col++)
grid[row][col] = ' ';
/* Plot balance points */
for (year = 0; year < total_years && year < graph_width; year++) {
int end_month = (year + 1) * 12;
double balance;
int plot_row;
if (end_month > tbl->num_payments) end_month = tbl->num_payments;
balance = tbl->schedule[end_month - 1].balance;
plot_row = (int)((1.0 - balance / max_val) * (graph_height - 1) + 0.5);
if (plot_row < 0) plot_row = 0;
if (plot_row >= graph_height) plot_row = graph_height - 1;
grid[plot_row][year] = '*';
/* Fill below the point with shading */
{
int r;
for (r = plot_row + 1; r < graph_height; r++)
grid[r][year] = ':';
}
}
/* Render */
printf(" %s REMAINING BALANCE%s\n", CB_WHITE, C_RESET);
color_line(C_SEPARATOR, '-', graph_width + 12);
for (row = 0; row < graph_height; row++) {
double val = max_val * (1.0 - (double)row / (graph_height - 1));
char amt[20];
format_currency(amt, val);
if (row == 0 || row == graph_height / 2 || row == graph_height - 1)
printf(" %s%10.10s%s %s|%s", C_DIM, amt, C_RESET, C_DIM, C_RESET);
else
printf(" %s %s %s|%s", C_DIM, C_RESET, C_DIM, C_RESET);
for (col = 0; col < graph_width; col++) {
if (grid[row][col] == '*')
printf("%s*%s", CB_CYAN, C_RESET);
else if (grid[row][col] == ':')
printf("%s:%s", C_BLUE, C_RESET);
else
putchar(' ');
}
printf("\n");
}
/* X axis */
printf(" %s %s %s+%s", C_DIM, C_RESET, C_DIM, C_RESET);
for (col = 0; col < graph_width; col++)
printf("%s-%s", C_DIM, C_RESET);
printf("\n");
/* X labels */
printf(" ");
for (year = 0; year < total_years && year < graph_width; year++) {
if ((year + 1) % 5 == 0 || year == 0)
printf("%s%-2d%s", C_CYAN, year + 1, C_RESET);
else
printf(" ");
}
printf("\n %s(Years)%s\n\n", C_DIM, C_RESET);
}
/* ════════════════════════════════════════════════════════════
GRAPH 3: Cumulative Interest Milestones
════════════════════════════════════════════════════════════ */
static void graph_interest_milestones(const AmortTable *tbl)
{
int total_years, year;
int bar_width = 50;
clear_screen();
printf("\n");
print_box_top(W);
print_box_empty(W);
print_box_line("C U M U L A T I V E I N T E R E S T", CB_CYAN, W);
print_box_line("Interest Paid Over Time", C_DIM, W);
print_box_empty(W);
print_box_bottom(W);
printf("\n");
print_loan_ref(tbl);
total_years = (tbl->term_months + 11) / 12;
printf(" %s CUMULATIVE INTEREST PAID%s\n", CB_WHITE, C_RESET);
color_line(C_SEPARATOR, '-', 72);
for (year = 0; year < total_years; year++) {
int end_month = (year + 1) * 12;
double cum_int;
double ratio;
int bars, b;
char amt[20];
if (end_month > tbl->num_payments) end_month = tbl->num_payments;
cum_int = tbl->schedule[end_month - 1].cumulative_interest;
ratio = (tbl->total_interest > 0.0) ? cum_int / tbl->total_interest : 0.0;
bars = (int)(ratio * bar_width + 0.5);
format_currency(amt, cum_int);
printf(" %sY%-2d%s %s|%s", CB_CYAN, year + 1, C_RESET, C_DIM, C_RESET);
/* Color gradient: green early -> yellow mid -> red late */
if (ratio < 0.33)
printf("%s", C_GREEN);
else if (ratio < 0.66)
printf("%s", C_YELLOW);
else
printf("%s", CB_RED);
for (b = 0; b < bars; b++) putchar('=');
printf("%s", C_RESET);
{
int spaces = bar_width - bars;
int s;
for (s = 0; s < spaces; s++) putchar(' ');
}
printf("%s|%s %s%s%s (%s%.0f%%%s)\n",
C_DIM, C_RESET, C_DIM, amt, C_RESET,
C_RATE, ratio * 100.0, C_RESET);
}
color_line(C_SEPARATOR, '-', 72);
/* Final total */
{
char ti[20];
format_currency(ti, tbl->total_interest);
printf(" %sTotal Interest Paid:%s %s%s%s\n\n",
CB_WHITE, C_RESET, CB_RED, ti, C_RESET);
}
/* Interest-to-principal ratio */
{
double ratio = (tbl->loan_amount > 0.0)
? tbl->total_interest / tbl->loan_amount * 100.0 : 0.0;
printf(" %sInterest-to-Principal Ratio:%s %s%.1f%%%s",
C_LABEL, C_RESET, CB_YELLOW, ratio, C_RESET);
printf(" %s(you pay %s$%.2f%s in interest for every %s$1.00%s borrowed)%s\n\n",
C_DIM, C_RED, tbl->total_interest / tbl->loan_amount, C_RESET,
C_GREEN, C_RESET, C_DIM);
}
}
/* ════════════════════════════════════════════════════════════
GRAPHS SUBMENU
════════════════════════════════════════════════════════════ */
static void print_graphs_menu(void)
{
printf(" %s+--------------------------------------------------------+%s\n", C_BOX, C_RESET);
printf(" %s|%s %s[1]%s Principal vs Interest %s[4]%s Back to Results %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s|%s %s[2]%s Balance Curve %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s|%s %s[3]%s Cumulative Interest %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s+--------------------------------------------------------+%s\n", C_BOX, C_RESET);
}
void graphs_menu(const AmortTable *tbl)
{
int choice;
/* Start with bar chart */
graph_bar_chart(tbl);
print_graphs_menu();
for (;;) {
choice = get_int(">");
switch (choice) {
case 1:
graph_bar_chart(tbl);
print_graphs_menu();
break;
case 2:
graph_balance_curve(tbl);
print_graphs_menu();
break;
case 3:
graph_interest_milestones(tbl);
print_graphs_menu();
break;
case 4:
return;
default:
printf(" %sInvalid choice. Try 1-4.%s\n", C_ERROR, C_RESET);
break;
}
}
}
/*
* members.h - Member management screens (list, add, load, delete)
*/
#ifndef MEMBERS_H
#define MEMBERS_H
#include "types.h"
/* Returns 1 if a member was loaded into tbl, 0 otherwise */
int members_menu(AmortTable *tbl);
#endif
/*
* members.c - Member management screens
*/
#include <stdio.h>
#include <string.h>
#include "types.h"
#include "util.h"
#include "calc.h"
#include "db.h"
#include "members.h"
static void print_members_header(void)
{
clear_screen();
printf("\n");
print_box_top(W);
print_box_empty(W);
print_box_line("M E M B E R S", CB_CYAN, W);
print_box_line("Saved Loan Profiles", C_DIM, W);
print_box_empty(W);
print_box_bottom(W);
printf("\n");
}
static void member_add(void)
{
AmortTable tbl;
memset(&tbl, 0, sizeof(tbl));
print_members_header();
printf(" %s ADD NEW MEMBER%s\n", CB_WHITE, C_RESET);
color_line(C_SEPARATOR, '-', 50);
get_string("Member name:", tbl.member_name, MAX_NAME);
if (tbl.member_name[0] == '\0') {
printf(" %sError: Name cannot be empty.%s\n", C_ERROR, C_RESET);
wait_for_enter();
return;
}
tbl.loan_amount = get_double("Loan amount ($):");
if (tbl.loan_amount <= 0.0) {
printf(" %sError: Loan amount must be positive.%s\n", C_ERROR, C_RESET);
wait_for_enter();
return;
}
tbl.annual_rate = get_double("Annual interest rate (%%):");
if (tbl.annual_rate < 0.0) {
printf(" %sError: Rate cannot be negative.%s\n", C_ERROR, C_RESET);
wait_for_enter();
return;
}
tbl.term_months = get_int("Loan term in months:");
if (tbl.term_months <= 0 || tbl.term_months > MAX_PAYMENTS) {
printf(" %sError: Term must be 1-%d months.%s\n", C_ERROR, MAX_PAYMENTS, C_RESET);
wait_for_enter();
return;
}
if (db_save_member(&tbl) == 0) {
char amt[32];
format_currency(amt, tbl.loan_amount);
printf("\n %sMember saved!%s ID: %s%d%s Name: %s%s%s Loan: %s%s%s\n",
CB_GREEN, C_RESET,
CB_CYAN, tbl.member_id, C_RESET,
CB_WHITE, tbl.member_name, C_RESET,
C_MONEY, amt, C_RESET);
} else {
printf(" %sError saving member.%s\n", C_ERROR, C_RESET);
}
wait_for_enter();
}
static int member_load(AmortTable *tbl)
{
int id, count;
print_members_header();
printf(" %s SELECT MEMBER TO LOAD%s\n\n", CB_WHITE, C_RESET);
count = db_list_members();
if (count <= 0) {
wait_for_enter();
return 0;
}
id = get_int("Enter member ID (0 to cancel):");
if (id <= 0) return 0;
if (db_load_member(id, tbl) != 0) {
printf(" %sMember ID %d not found.%s\n", C_ERROR, id, C_RESET);
wait_for_enter();
return 0;
}
/* Generate the schedule from loaded data */
if (generate_schedule(tbl) != 0) {
printf(" %sError generating schedule.%s\n", C_ERROR, C_RESET);
wait_for_enter();
return 0;
}
{
char amt[32];
format_currency(amt, tbl->loan_amount);
printf("\n %sLoaded:%s %s%s%s Loan: %s%s%s Rate: %s%.3f%%%s Term: %s%d mo%s\n",
CB_GREEN, C_RESET,
CB_WHITE, tbl->member_name, C_RESET,
C_MONEY, amt, C_RESET,
C_RATE, tbl->annual_rate, C_RESET,
C_VALUE, tbl->term_months, C_RESET);
}
wait_for_enter();
return 1;
}
static void member_delete(void)
{
int id, count;
char confirm[8];
print_members_header();
printf(" %s DELETE MEMBER%s\n\n", CB_WHITE, C_RESET);
count = db_list_members();
if (count <= 0) {
wait_for_enter();
return;
}
id = get_int("Enter member ID to delete (0 to cancel):");
if (id <= 0) return;
printf(" %sAre you sure? (y/n):%s ", CB_RED, C_RESET);
fflush(stdout);
if (fgets(confirm, sizeof(confirm), stdin) == NULL) return;
if (confirm[0] == 'y' || confirm[0] == 'Y') {
if (db_delete_member(id) == 0)
printf(" %sMember %d deleted.%s\n", CB_GREEN, id, C_RESET);
else
printf(" %sMember %d not found.%s\n", C_ERROR, id, C_RESET);
} else {
printf(" %sCancelled.%s\n", C_DIM, C_RESET);
}
wait_for_enter();
}
static void member_view_all(void)
{
print_members_header();
printf(" %s ALL MEMBERS%s\n\n", CB_WHITE, C_RESET);
db_list_members();
wait_for_enter();
}
int members_menu(AmortTable *tbl)
{
int choice;
int loaded = 0;
for (;;) {
print_members_header();
{
int count = db_count_members();
printf(" %s%d member(s) in database%s\n\n", C_DIM, count, C_RESET);
}
printf(" %s+--------------------------------------------------+%s\n", C_BOX, C_RESET);
printf(" %s|%s %s[1]%s View All Members %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s|%s %s[2]%s Add New Member %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s|%s %s[3]%s Load Member (view loan) %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s|%s %s[4]%s Delete Member %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s|%s %s[5]%s Back to Main Menu %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s+--------------------------------------------------+%s\n", C_BOX, C_RESET);
choice = get_int(">");
switch (choice) {
case 1:
member_view_all();
break;
case 2:
member_add();
break;
case 3:
if (member_load(tbl)) {
loaded = 1;
return loaded;
}
break;
case 4:
member_delete();
break;
case 5:
return loaded;
default:
printf(" %sInvalid choice. Try 1-5.%s\n", C_ERROR, C_RESET);
wait_for_enter();
break;
}
}
}
/*
* main.c - Entry point: main menu, loan input, results menu
*
* Amortization Schedule Calculator - Modular ANSI C Edition
* Compile: make (or see Makefile)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "types.h"
#include "util.h"
#include "calc.h"
#include "db.h"
#include "dashboard.h"
#include "schedule.h"
#include "graphs.h"
#include "members.h"
/* ── Main Menu ── */
static void print_main_menu(void)
{
int count;
clear_screen();
printf("\n");
print_box_top(W);
print_box_empty(W);
print_box_line("A M O R T I Z A T I O N C A L C U L A T O R", CB_CYAN, W);
print_box_line("ANSI C Edition", C_DIM, W);
print_box_empty(W);
print_box_sep(W);
print_box_empty(W);
print_box_line("[1] New Loan Calculation", C_MENU_TXT, W);
print_box_line("[2] Members (Saved Loans)", C_MENU_TXT, W);
print_box_line("[3] Quit", C_MENU_TXT, W);
print_box_empty(W);
print_box_bottom(W);
count = db_count_members();
printf("\n %s%d member(s) in database%s\n\n", C_DIM, count, C_RESET);
}
/* ── Loan Input (manual entry) ── */
static int get_loan_input(AmortTable *tbl)
{
clear_screen();
printf("\n");
print_box_top(W);
print_box_empty(W);
print_box_line("N E W L O A N", CB_CYAN, W);
print_box_empty(W);
print_box_bottom(W);
printf("\n");
memset(tbl, 0, sizeof(*tbl));
tbl->loan_amount = get_double("Loan amount ($):");
if (tbl->loan_amount <= 0.0) {
printf(" %sError: Loan amount must be positive.%s\n", C_ERROR, C_RESET);
wait_for_enter();
return -1;
}
tbl->annual_rate = get_double("Annual interest rate (%%):");
if (tbl->annual_rate < 0.0) {
printf(" %sError: Rate cannot be negative.%s\n", C_ERROR, C_RESET);
wait_for_enter();
return -1;
}
tbl->term_months = get_int("Loan term in months:");
if (tbl->term_months <= 0 || tbl->term_months > MAX_PAYMENTS) {
printf(" %sError: Term must be 1-%d months.%s\n", C_ERROR, MAX_PAYMENTS, C_RESET);
wait_for_enter();
return -1;
}
if (generate_schedule(tbl) != 0) {
printf(" %sError: Could not generate schedule.%s\n", C_ERROR, C_RESET);
wait_for_enter();
return -1;
}
/* Offer to save as member */
{
char yn[8];
printf("\n %sSave as member? (y/n):%s ", C_PROMPT, C_RESET);
fflush(stdout);
if (fgets(yn, sizeof(yn), stdin) != NULL && (yn[0] == 'y' || yn[0] == 'Y')) {
get_string("Member name:", tbl->member_name, MAX_NAME);
if (tbl->member_name[0] != '\0') {
if (db_save_member(tbl) == 0) {
printf(" %sSaved as member #%d!%s\n", CB_GREEN, tbl->member_id, C_RESET);
} else {
printf(" %sError saving.%s\n", C_ERROR, C_RESET);
}
wait_for_enter();
}
}
}
return 0;
}
/* ── Results Menu ── */
static void print_results_menu(const AmortTable *tbl)
{
if (tbl->member_id > 0) {
printf(" %sActive: %s%s%s (ID %s%d%s)%s\n",
C_DIM, CB_WHITE, tbl->member_name, C_DIM,
CB_CYAN, tbl->member_id, C_DIM, C_RESET);
}
printf(" %s+--------------------------------------------------------+%s\n", C_BOX, C_RESET);
printf(" %s|%s %s[1]%s Dashboard %s[5]%s New Loan %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s|%s %s[2]%s Monthly Schedule %s[6]%s Members %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s|%s %s[3]%s Graphs %s[7]%s Quit %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s|%s %s[4]%s Save as Member %s|%s\n",
C_BOX, C_RESET, C_MENU_KEY, C_MENU_TXT, C_BOX, C_RESET);
printf(" %s+--------------------------------------------------------+%s\n", C_BOX, C_RESET);
}
static void results_menu(AmortTable *tbl)
{
int choice;
print_dashboard(tbl);
print_results_menu(tbl);
for (;;) {
choice = get_int(">");
switch (choice) {
case 1:
print_dashboard(tbl);
print_results_menu(tbl);
break;
case 2:
print_monthly_schedule(tbl);
print_dashboard(tbl);
print_results_menu(tbl);
break;
case 3:
graphs_menu(tbl);
print_dashboard(tbl);
print_results_menu(tbl);
break;
case 4:
/* Save current loan as member */
clear_screen();
printf("\n");
print_box_top(W);
print_box_empty(W);
print_box_line("S A V E M E M B E R", CB_CYAN, W);
print_box_empty(W);
print_box_bottom(W);
printf("\n");
if (tbl->member_id > 0) {
/* Already saved — update */
if (db_update_member(tbl) == 0)
printf(" %sMember #%d (%s) updated.%s\n",
CB_GREEN, tbl->member_id, tbl->member_name, C_RESET);
else
printf(" %sUpdate failed.%s\n", C_ERROR, C_RESET);
} else {
get_string("Member name:", tbl->member_name, MAX_NAME);
if (tbl->member_name[0] != '\0') {
if (db_save_member(tbl) == 0)
printf("\n %sSaved as member #%d!%s\n",
CB_GREEN, tbl->member_id, C_RESET);
else
printf(" %sError saving.%s\n", C_ERROR, C_RESET);
}
}
wait_for_enter();
print_dashboard(tbl);
print_results_menu(tbl);
break;
case 5:
return; /* back to main menu */
case 6:
members_menu(tbl);
/* Always redraw dashboard on return */
print_dashboard(tbl);
print_results_menu(tbl);
break;
case 7:
db_close();
clear_screen();
printf("\n %sGoodbye!%s\n\n", CB_CYAN, C_RESET);
exit(0);
default:
clear_screen();
print_dashboard(tbl);
print_results_menu(tbl);
printf(" %sInvalid choice. Try 1-7.%s\n", C_ERROR, C_RESET);
break;
}
}
}
/* ── Main ── */
int main(void)
{
AmortTable tbl;
int choice;
if (db_init() != 0) {
printf("Failed to initialize database.\n");
return 1;
}
for (;;) {
print_main_menu();
choice = get_int("Select:");
switch (choice) {
case 1:
if (get_loan_input(&tbl) == 0) {
results_menu(&tbl);
}
break;
case 2:
if (members_menu(&tbl)) {
results_menu(&tbl);
}
break;
case 3:
db_close();
clear_screen();
printf("\n %sGoodbye!%s\n\n", CB_CYAN, C_RESET);
return 0;
default:
printf(" %sInvalid choice.%s\n", C_ERROR, C_RESET);
wait_for_enter();
break;
}
}
return 0;
}
Instead of copying files one by one, you can generate a shell script that creates all 17 files at once. Click below to copy it, then paste into your terminal.
cat << 'EOF' heredocs to write each file. It creates the directory structure, writes all 17 files, and runs make automatically.The loan_members.db SQLite database is created automatically on first run in the working directory. Members persist between sessions.
make again — it only recompiles changed files. Use make clean to remove all build artifacts.