Back to ANSI C Examples
Build Guide

Amortization Calculator
Setup & Source Code

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.

amortization — 90×24
+========================================================================================+ || || || A M O R T I Z A T I O N C A L C U L A T O R || || ANSI C Edition || || || ||--------------------------------------------------------------------------------------|| || || || [1] New Loan Calculation || || [2] Members (Saved Loans) || || [3] Quit || || || +========================================================================================+ 1 member(s) in database Select:
The main menu — running on macOS Terminal with full ANSI color support
amortization — dashboard
+========================================================================================+ || || || D A S H B O A R D || || || +========================================================================================+ ========================================================================================== LOAN SUMMARY ------------------------------------------------------------------------------------------ Loan Amount: $650,000.00 Monthly Payment: $3,690.63 Annual Rate: 5.500 % Term: 360 mo (30y 0m) Total Paid: $1,328,626.26 Total Interest: $678,626.26 (51.1%) ========================================================================================== ANNUAL BREAKDOWN ------------------------------------------------------------------------------ Year Principal Interest End Balance Paid Off ------------------------------------------------------------------------------ 1 $8,756.08 $35,531.46 $641,243.92 1.3% 2 $9,249.99 $35,037.55 $631,993.93 2.8% 3 $9,771.77 $34,515.78 $622,222.16 4.3% 4 $10,322.97 $33,964.57 $611,899.19 5.9% 5 $10,905.27 $33,382.28 $600,993.92 7.5% 6 $11,520.41 $32,767.13 $589,473.51 9.3% 7 $12,170.25 $32,117.29 $577,303.26 11.2% 8 $12,856.75 $31,430.79 $564,446.51 13.2% 9 $13,581.97 $30,705.57 $550,864.54 15.3% 10 $14,348.10 $29,939.44 $536,516.44 17.5% 11 $15,157.45 $29,130.10 $521,359.00 19.8% 12 $16,012.45 $28,275.10 $505,346.55 22.3% 13 $16,915.67 $27,371.87 $488,430.88 24.9% 14 $17,869.85 $26,417.69 $470,561.03 27.6% 15 $18,877.85 $25,409.69 $451,683.18 30.5% 16 $19,942.71 $24,344.83 $431,740.47 33.6% 17 $21,067.63 $23,219.91 $410,672.83 36.8% 18 $22,256.01 $22,031.53 $388,416.82 40.2% 19 $23,511.43 $20,776.11 $364,905.39 43.9% 20 $24,837.66 $19,449.88 $340,067.73 47.7% 21 $26,238.70 $18,048.84 $313,829.03 51.7% 22 $27,718.77 $16,568.78 $286,110.27 56.0% 23 $29,282.32 $15,005.22 $256,827.94 60.5% 24 $30,934.08 $13,353.47 $225,893.87 65.2% 25 $32,679.00 $11,608.54 $193,214.87 70.3% 26 $34,522.35 $9,765.19 $158,692.51 75.6% 27 $36,469.69 $7,817.86 $122,222.83 81.2% 28 $38,526.86 $5,760.68 $83,695.97 87.1% 29 $40,700.08 $3,587.46 $42,995.88 93.4% 30 $42,995.88 $1,291.66 $0.00 100.0% ------------------------------------------------------------------------------ +--------------------------------------------------------+ | [1] Dashboard [4] New Loan | | [2] Monthly Schedule [5] Quit | | [3] Principal vs Interest | +--------------------------------------------------------+ >
The dashboard — loan summary, annual breakdown with color-coded payoff progress, and navigation menu
STEP 01

Prerequisites

You need a C compiler and the SQLite3 development library.

Ubuntu / Debian (VPS)

$ sudo apt install build-essential libsqlite3-dev

macOS

$ xcode-select --install

macOS ships with SQLite3 built in — no extra install needed.

STEP 02

Project Structure

Create the project folder and the src/ subdirectory:

$ mkdir -p ~/amortization/src && cd ~/amortization

The final structure looks like this:

amortization/ ├── Makefile └── src/ ├── main.c — entry point, menus ├── types.h — shared structs & colors ├── util.c / .h — TUI helpers ├── calc.c / .h — amortization math ├── db.c / .h — SQLite3 member CRUD ├── dashboard.c/.h — dashboard screen ├── schedule.c/.h — monthly schedule ├── graphs.c / .h — 3 graph types └── members.c / .h — member management
STEP 03

Source Files

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.

17 files — click to expand, copy individually, or use the setup script
BUILD Makefile 52 lines
Build configuration — works on Linux and macOS
# 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."
HEADER src/types.h 80 lines
Shared structs, constants, ANSI color macros
/*
 * 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 */
HEADER src/util.h 21 lines
Screen, box drawing, formatting, input prototypes
/*
 * 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
SOURCE src/util.c 137 lines
Screen clearing, box drawing, currency formatting, input helpers
/*
 * 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';
}
HEADER src/calc.h 13 lines
Amortization math function prototypes
/*
 * 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
SOURCE src/calc.c 60 lines
Amortization formula and schedule generation engine
/*
 * 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;
}
HEADER src/db.h 19 lines
SQLite3 database operation prototypes
/*
 * 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
SOURCE src/db.c 202 lines
SQLite3 CRUD — init, save, load, update, delete, list members
/*
 * 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;
}
HEADER src/dashboard.h 12 lines
Dashboard screen prototype
/*
 * dashboard.h - Dashboard screen
 */
#ifndef DASHBOARD_H
#define DASHBOARD_H

#include "types.h"

void print_dashboard(const AmortTable *tbl);

#endif
SOURCE src/dashboard.c 110 lines
Dashboard screen — loan summary card + annual breakdown table
/*
 * 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");
}
HEADER src/schedule.h 12 lines
Monthly schedule screen prototype
/*
 * schedule.h - Paginated monthly schedule screen
 */
#ifndef SCHEDULE_H
#define SCHEDULE_H

#include "types.h"

void print_monthly_schedule(const AmortTable *tbl);

#endif
SOURCE src/schedule.c 86 lines
Paginated monthly amortization schedule (24 rows/page)
/*
 * 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;
        }
    }
}
HEADER src/graphs.h 12 lines
Graphs submenu prototype
/*
 * 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
SOURCE src/graphs.c 340 lines
Graphs submenu — bar chart, balance curve, cumulative interest
/*
 * 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;
        }
    }
}
HEADER src/members.h 13 lines
Member management screen prototype
/*
 * 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
SOURCE src/members.c 212 lines
Member management — view all, add, load, delete
/*
 * 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;
        }
    }
}
SOURCE src/main.c 245 lines
Entry point — main menu, loan input, results menu routing
/*
 * 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;
}
STEP 04

One-Command Setup Script

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.

Creates ~/amortization/ with all source files
The script uses cat << 'EOF' heredocs to write each file. It creates the directory structure, writes all 17 files, and runs make automatically.
STEP 05

Build & Run

$ cd ~/amortization && make && ./amortization

The loan_members.db SQLite database is created automatically on first run in the working directory. Members persist between sessions.

If you edit a single file, just run make again — it only recompiles changed files. Use make clean to remove all build artifacts.