---
title: "TaxPal — Accounting Brokerage Sales Dashboard — Tran Manh Tuan"
description: "Sales analytics for an accounting-services brokerage in Japan: cases, KPI targets against actuals, rankings and pipeline conversion by staff member, team and company."
canonical: "https://tmtuan.dev/en/projects/taxpal"
language: "en"
image: "https://tmtuan.dev/_img/projects/taxpal/taxpal.ac7aeb13-og.jpg"
tokens: 5329
---

# TaxPal

**TaxPal** is an accounting-services brokerage platform in Japan. It connects three sides: customers who need an accountant or another expert, the sales and broker team who take each request and see it through, and the accountants and experts who deliver the service. Each side has its own area of the product, and all of them share the same companies, cases and service history.

In 2022 I was the **Tech Lead** of the team in Vietnam and coordinated the Vietnam side of the project through its main development phase. Our scope was the **sales dashboard**: the internal analytics the broker team and its managers use to follow cases, KPIs, sales performance and conversion — for each staff member, each team and the whole company.

> **Why it is a management-system project:** the hard part was never drawing charts. It was deciding what a number means — which cases count, on which working days, in which fiscal period, for staff who have since left — and then serving those numbers quickly. It was also brownfield work: new dashboards on an existing AWS codebase, with its own GraphQL resolvers, database, authentication and release process.

## The platform and who uses it

Customers bring a need. The sales and broker team turn it into a case, follow it up and move it towards a contract; their managers watch individual, team and company performance. Accountants and experts take the cases that reach them and have their own dashboard.

```mermaid
flowchart LR
    accTitle: The TaxPal ecosystem
    accDescr: Customers who need a service enter through the customer area, which creates company, customer and case data. Cases go to the sales and broker team, who work in the sales area; that area updates cases and leads to the sales dashboard. Cases also go to accountants and experts, who use the accountant / expert area and the expert dashboard.

    CUSTOMER["Customers needing a service"]
    SALES["Sales / broker team"]
    EXPERT["Accountants / experts"]

    CUSTOMER_PORTAL["Customer area"]
    SALES_PORTAL["Sales / broker area"]
    EXPERT_PORTAL["Accountant / expert area"]

    CASE["Company / customer / case"]
    DASH["Sales dashboard"]
    EXPERT_DASH["Expert dashboard"]

    CUSTOMER --> CUSTOMER_PORTAL
    CUSTOMER_PORTAL --> CASE

    CASE --> SALES
    SALES --> SALES_PORTAL

    SALES_PORTAL --> CASE
    SALES_PORTAL --> DASH

    CASE --> EXPERT
    EXPERT --> EXPERT_PORTAL
    EXPERT_PORTAL --> EXPERT_DASH
```

_Three user groups, one shared set of companies and cases. The sales dashboard sits on the broker side of the product._

## My responsibilities

- Tracked and controlled the team's technical progress, schedule and workload across menus and milestones.
- Coordinated backend, frontend and the IT communicator, and met the Japanese client regularly.
- Turned business requirements into technical designs; broke down and estimated the work.
- Reviewed API designs and pull requests.
- Unblocked hard problems in GraphQL, Prisma, the database and integration, and coordinated bug fixes between frontend and backend.
- Assessed schedule risk and cleared up unclear points in the business documents with the client.
- Managed hand-over and releases through the client's repository process, and reported status, issues, causes and fixes.

## From request to contract: the case pipeline

Every customer need is a **case**, linked to a company record that becomes the master data for its appointments, progress and contracts. A salesperson can be assigned by hand — the system does not force an automatic assignment rule on the organisation. From there a case moves through many sales states: likely to get an appointment, blocked by an issue, difficult, contacted but not reached, appointment confirmed or cancelled, following up, contract signed, accounting contract signed, paused, cancelled, cancelled at renewal. In the version described, salespeople choose the status themselves on the case screen rather than a fully automatic state machine setting it.

```mermaid
flowchart TD
    accTitle: How a case moves through the pipeline
    accDescr: A customer need arrives, the system creates or finds the company, creates a case and assigns a salesperson, who contacts the customer. If they cannot connect, they retry and contact again. Once connected, they keep following up and judge whether an appointment is possible: if not, the case stops and is closed; if so, the appointment is confirmed and followed by a meeting and follow-up. If it moves to a contract, a contract and then an accounting contract or service follow; otherwise the case is cancelled or closed. This is a simplified version of the real status system.

    A["A customer need arrives"]
    B["Create / find the company"]
    C["Create the case"]
    D["Assign a salesperson"]
    E["Contact the customer"]

    F{"Connected?"}
    G["Keep following up"]
    H["Not reached / retry"]

    I{"Appointment possible?"}
    J["Confirm the appointment"]
    K["Not feasible / stop"]

    L["Meeting / follow-up"]
    M{"Moving to a contract?"}
    N["Contract"]
    O["Accounting contract / service"]
    P["Cancel / close"]

    A --> B
    B --> C
    C --> D
    D --> E

    E --> F
    F -- "No" --> H
    H --> E

    F -- "Yes" --> G
    G --> I

    I -- "No" --> K
    I -- "Yes" --> J

    J --> L
    L --> M

    M -- "Yes" --> N
    N --> O

    M -- "No" --> P
    K --> P
```

_A simplified view of the real status system. The case, not the customer, is the unit every count, KPI and conversion rate is calculated from._

## What the sales dashboard shows

The dashboard turns cases into management views: new and in-progress cases, intake, KPIs, actual results, sales, success rates, and performance per staff member, per team and for the company. It includes a weekly table of cases per member and team, KPI results by day, month and year, a sales ranking, sales by service over the last four years, and the success rate of the pipeline over periods from two weeks to a year.

```mermaid
flowchart TB
    accTitle: Data flow of the sales dashboard
    accDescr: Six sources — case and company data, the staff master, the team master, KPI and sales targets, working days and holidays, and sales status — all feed a GraphQL analytics API. The API passes the data through an aggregation and business-rule layer that produces three views: by staff member, by team and for the whole company. The staff view renders as tables and a sales ranking; the team and company views render as charts.

    CASE["Case / company data"]
    STAFF["Staff master"]
    TEAM["Team master"]
    TARGET["KPI / sales target"]
    HOLIDAY["Working day / holiday"]
    STATUS["Sales status"]

    API["GraphQL analytics API"]

    AGG["Aggregation / business rules"]

    STAFF_VIEW["By staff member"]
    TEAM_VIEW["By team"]
    COMPANY_VIEW["Whole company"]

    TABLE["Tables"]
    CHART["Charts"]
    RANK["Sales ranking"]

    CASE --> API
    STAFF --> API
    TEAM --> API
    TARGET --> API
    HOLIDAY --> API
    STATUS --> API

    API --> AGG

    AGG --> STAFF_VIEW
    AGG --> TEAM_VIEW
    AGG --> COMPANY_VIEW

    STAFF_VIEW --> TABLE
    TEAM_VIEW --> CHART
    COMPANY_VIEW --> CHART
    STAFF_VIEW --> RANK
```

_Six sources, one analytics API, one layer of business rules, three levels of aggregation._

## KPI targets against actuals

Managers enter sales targets by year, month, staff member and metric. An empty period opens ready for input; a period that already has targets opens them for editing. Targets are then compared with actual results at daily, monthly and yearly scale, so the gap and its trend are visible, not only the total.

```mermaid
flowchart LR
    accTitle: KPI target against actual
    accDescr: A manager or sales user enters KPI targets, which are stored as sales target data. In parallel, case, sales and contract data are used to calculate the actual figures. Targets and actuals meet in a target-against-actual comparison, shown at three time scales: daily, monthly and yearly.

    MANAGER["Manager / sales user"]
    INPUT["Enter KPI targets"]
    TARGET_DB["Sales target data"]

    BUSINESS["Case / sales / contract data"]
    ACTUAL["Calculate actuals"]

    COMPARE["Target vs actual"]
    DAILY["Daily"]
    MONTHLY["Monthly"]
    YEARLY["Yearly"]

    MANAGER --> INPUT
    INPUT --> TARGET_DB

    BUSINESS --> ACTUAL

    TARGET_DB --> COMPARE
    ACTUAL --> COMPARE

    COMPARE --> DAILY
    COMPARE --> MONTHLY
    COMPARE --> YEARLY
```

_Targets are entered once; actuals come from the same case, sales and contract data the rest of the platform uses._

## Counting a customer once: the duplicate-case rule

The same customer can arrive more than once, through different contacts or at different times. The rule groups cases that share a representative, a matching email or phone number, and an intake date inside a set window. Groups can be chains: if A matches B and B matches C, all three belong together. For each group the dashboard picks one representative record and one forecast value, so mail, contact and case counts are not inflated.

```mermaid
flowchart TD
    accTitle: The duplicate-case rule
    accDescr: The system collects cases that may be related and checks three conditions in turn: the same representative, a matching email or phone number, and an intake date inside the rule's window. A case that fails any condition is handled as an independent case. Cases that pass all three go into a duplicate group; the system checks chained relations such as A-B-C, picks a representative record, picks the forecast value by rule, and counts the whole group as one case in the statistics.

    A["Collect cases that may be related"]
    B{"Same representative?"}
    C{"Email / phone match?"}
    D{"Intake date inside the rule?"}

    E["Put into a duplicate group"]
    F["Check chained relations A-B-C"]
    G["Pick a representative record"]
    H["Pick the forecast value by rule"]
    I["Count the group as one case"]

    X["Handle as an independent case"]

    A --> B

    B -- "No" --> X
    B -- "Yes" --> C

    C -- "No" --> X
    C -- "Yes" --> D

    D -- "No" --> X
    D -- "Yes" --> E

    E --> F
    F --> G
    G --> H
    H --> I
```

_A dashboard figure is only as good as its counting rule. Duplicates are resolved before anything is summed._

## Numbers that respect the calendar and the organisation

- **Read-optimised analytics tables.** To avoid recomputing everything from transactional tables on every page load, statistics tables store what aggregation needs: the period and its start and end dates, team order, the type of statistic, staff status and working dates. Reads get fast and queries simple; the trade-off is that a changed business rule can mean rebuilding that data.
- **Working days.** Remaining working days and valid intake dates are counted against holiday data, not the plain calendar.
- **Fiscal periods.** A year chosen on screen can mean a fiscal year running from October to September, so frontend and backend must agree on the date range rather than filter by calendar year.
- **Staff who have left.** A retired flag lives in the statistics data, so historical reports keep former staff without treating them as active in every calculation.

## The expert dashboard

The project documentation also covers a dashboard for the accountants and experts themselves: income, business performance, strategy, people, succession, IT adoption and customer satisfaction, compared on radar charts against individual, group, company and reference-average benchmarks.

```mermaid
flowchart TB
    accTitle: Structure of the expert dashboard
    accDescr: The expert dashboard has seven groups of screens: income, business performance, strategy, people, succession, IT adoption and customer satisfaction. All seven feed a radar chart and benchmark for comparison across dimensions. The overview page also leads to search and reference.

    HOME["Expert dashboard"]

    EARN["Income"]
    EFF["Business performance"]
    STRATEGY["Strategy"]
    HR["People"]
    INHERIT["Succession"]
    IT["IT adoption"]
    CS["Customer satisfaction"]

    RADAR["Radar chart / benchmark"]
    SEARCH["Search / reference"]

    HOME --> EARN
    HOME --> EFF
    HOME --> STRATEGY
    HOME --> HR
    HOME --> INHERIT
    HOME --> IT
    HOME --> CS

    EARN --> RADAR
    EFF --> RADAR
    STRATEGY --> RADAR
    HR --> RADAR
    INHERIT --> RADAR
    IT --> RADAR
    CS --> RADAR

    HOME --> SEARCH
```

_Analytics is not only for the internal sales team: the experts get their own view across seven dimensions._

## Architecture

The frontend never touches the database. Next.js and React call AWS AppSync through Apollo; the GraphQL schema is authorised with Cognito user pools and IAM; each request goes through resolvers, use cases and repositories, and Prisma reaches PostgreSQL on AWS. The codebase also includes Elasticsearch / OpenSearch for search and indexing, and the AWS infrastructure is defined in Terraform.

```mermaid
flowchart TB
    accTitle: TaxPal high-level technical architecture
    accDescr: The browser runs a Next.js, React and TypeScript application, which calls an Apollo GraphQL client, which calls AWS AppSync GraphQL. Cognito and IAM authorise AppSync. AppSync passes requests through a resolver layer, use cases or interactors, and repositories. Repositories read and write PostgreSQL on AWS through Prisma, and work with Elasticsearch or OpenSearch. Terraform defines the AWS infrastructure for AppSync, Cognito and IAM, PostgreSQL and search. This is a portfolio-level diagram based on components in the codebase and documentation, not a copy of the production infrastructure diagram.

    USER["Browser"]

    NEXT["Next.js / React / TypeScript"]
    APOLLO["Apollo / GraphQL client"]

    APPSYNC["AWS AppSync GraphQL"]

    AUTH["Cognito / IAM"]
    RESOLVER["Resolver layer"]
    USECASE["Use case / interactor"]
    REPO["Repository"]

    PRISMA["Prisma"]
    POSTGRES["PostgreSQL on AWS"]

    SEARCH["Elasticsearch / OpenSearch"]
    INFRA["Terraform / AWS infrastructure"]

    USER --> NEXT
    NEXT --> APOLLO
    APOLLO --> APPSYNC

    AUTH --> APPSYNC

    APPSYNC --> RESOLVER
    RESOLVER --> USECASE
    USECASE --> REPO

    REPO --> PRISMA
    PRISMA --> POSTGRES

    REPO --> SEARCH

    INFRA --> APPSYNC
    INFRA --> AUTH
    INFRA --> POSTGRES
    INFRA --> SEARCH
```

_A portfolio-level view built from the components in the codebase and documents — not a copy of the production infrastructure diagram._

Adding an API meant the same path every time: schema, code generation, resolver, use case, repository, Prisma query, then test and seed data. Pull requests were reviewed on the client's side and every GitHub check had to pass. Database migrations and seed data went in pull requests of their own, so a schema change was never merged together with business code that would be hard to review or roll back.

```mermaid
flowchart TB
    accTitle: Building one GraphQL API
    accDescr: From a screen or business requirement, the team designs the API, writes the GraphQL schema, runs GraphQL code generation, then writes the resolver, the use case or interactor, the repository and the Prisma query against PostgreSQL. Test and seed data come next, then a pull request.

    REQ["Screen / business requirement"]
    DESIGN["API design"]

    SCHEMA["GraphQL schema"]
    CODEGEN["GraphQL codegen"]
    RESOLVER["Resolver"]
    USECASE["Use case / interactor"]
    REPOSITORY["Repository"]
    PRISMA["Prisma query"]
    DB["PostgreSQL"]

    TEST["Test / seed data"]
    PR["Pull request"]

    REQ --> DESIGN
    DESIGN --> SCHEMA
    SCHEMA --> CODEGEN
    CODEGEN --> RESOLVER

    RESOLVER --> USECASE
    USECASE --> REPOSITORY
    REPOSITORY --> PRISMA
    PRISMA --> DB

    DB --> TEST
    TEST --> PR
```

_One path for every new API, from requirement to pull request._

## Making the dashboard fast

A dashboard with many widgets and many statistics makes performance a daily concern. The issues found were concrete: APIs selecting far more fields than the screen used, whole relations loaded where a few fields would do, checks made with repeated requests, a screen of about 120 records taking more than a second, and some dashboards calling the API once per team. The fixes: select only what is needed, use relations instead of repeated queries, shrink responses, return several teams from one API, send fewer requests from the frontend, and lazy-load widgets the user has not reached yet — with loading states that tell "still loading" apart from "no data".

```mermaid
flowchart TD
    accTitle: Optimising the dashboard API
    accDescr: A dashboard widget can fetch its data two ways. The unoptimised way sends many requests, one per team, and selects too many fields and relations, so responses are slow. The optimised design selects only the data it needs, cuts repeated queries by using relations, aggregates several teams in one API and lazy-loads widgets, giving fewer requests, smaller payloads and faster loading.

    FE["Dashboard widget"]

    OLD["Unoptimised"]
    MANY["Many requests / one per team"]
    OVER["Selects too many fields / relations"]
    SLOW["Slow responses"]

    OPT["Optimised design"]
    SELECT["Select only what is needed"]
    RELATION["Fewer repeated queries via relations"]
    GROUP["Aggregate several teams in one API"]
    LAZY["Lazy-load widgets"]

    RESULT["Fewer requests / smaller payloads / faster load"]

    FE --> OLD
    OLD --> MANY
    MANY --> OVER
    OVER --> SLOW

    FE --> OPT
    OPT --> SELECT
    SELECT --> RELATION
    RELATION --> GROUP
    GROUP --> LAZY
    LAZY --> RESULT
```

_The same widget, fetched two ways. Most of the gain came from asking for less, less often._

## Charts and QA for a dashboard people manage by

A dashboard people manage by has to survive awkward data. Team colours had to stay readable as the number of teams grew, with teams beyond a threshold sharing one style, and some statistics showing only teams that had members. Values above 100% — up to around 200% — had to stay inside their chart, with readable tooltips and scale rules. Testing therefore went beyond the interface: field mapping, totals, working days, duplicate cases, sorting, statuses, date ranges, filters, zero and null values, and the structure of every API response.

## What the project covers

| Concern in a large management system | How it shows up in TaxPal |
| --- | --- |
| Sales pipeline with many states | Case statuses from first contact to contract, set by the salespeople |
| KPI management | Targets by year, month, staff member and metric; target against actual by day, month and year |
| Multi-level aggregation | Staff, team and company views; rankings; sales by service over four years |
| Data-correctness rules | Duplicate-case groups, working days, fiscal periods, retired staff |
| Read performance | Pre-computed analytics tables, selecting only what is needed, one API for many teams, lazy loading |
| Brownfield delivery | New modules on an existing AppSync, Prisma and PostgreSQL codebase; migrations in their own pull requests |
| Working with a client in Japan | Regular meetings, requirement clarification, releases through the client's repository |

The diagrams are reconstructed at component level from the project's documents. They omit internal URLs, identifiers, proprietary code and client data. The screenshots are design mock-ups and a redrawn data model; staff names, a logo and a portrait are blurred.

## Details

- **Role:** Technical Lead · Backend

## Technology

- Next.js
- React
- TypeScript
- Apollo Client
- GraphQL
- AWS AppSync
- AWS Cognito
- AWS Lambda
- PostgreSQL
- Prisma
- Elasticsearch
- Terraform
- Docker

## Services this project shows

- [Custom Business Systems](https://tmtuan.dev/en/services/business-systems)
- [Backend Engineering](https://tmtuan.dev/en/services/backend-development)
- [Production Troubleshooting](https://tmtuan.dev/en/services/production-troubleshooting)

## Related articles

- [Docs-as-code Knowledge Base + Context Graph for a 15-project portfolio](https://tmtuan.dev/en/articles/knowledge-base-docs-as-code-context-graph)
- [How I Traced a Googlebot-only HTTP 500 Across Next.js, Cloud Run and the Hosting Edge](https://tmtuan.dev/en/articles/googlebot-500-firebase-hosting-nextjs)

## Structured data (JSON-LD)

```json
{"@context":"https://schema.org","@graph":[{"@type":"WebPage","@id":"https://tmtuan.dev/en/projects/taxpal#webpage","url":"https://tmtuan.dev/en/projects/taxpal","name":"TaxPal — Accounting Brokerage Sales Dashboard","description":"Sales analytics for an accounting-services brokerage in Japan: cases, KPI targets against actuals, rankings and pipeline conversion by staff member, team and company.","inLanguage":"en","isPartOf":{"@id":"https://tmtuan.dev/#website"},"about":{"@id":"https://tmtuan.dev/#person"}},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://tmtuan.dev/en"},{"@type":"ListItem","position":2,"name":"Projects","item":"https://tmtuan.dev/en/projects"},{"@type":"ListItem","position":3,"name":"TaxPal","item":"https://tmtuan.dev/en/projects/taxpal"}]}]}
```
