Client Management is the cornerstone of the NDSS CRM platform, providing comprehensive tools for recording, organising, and managing NDIS participant information. This module handles everything from initial client creation through ongoing care plan management, NDIS funding tracking, goal monitoring, and progress documentation. Built on Next.js and React for the interface with Python and PHP microservices handling business logic, and PostgreSQL for data persistence, the Client Management module is designed to support organisations of any size, from small community providers to large multi-site operations.
The Client List View is the primary entry point for client management. It displays a paginated, searchable, and filterable table of all clients within the organisation. The list supports server-side pagination (20 records per page by default), full-text search, and multi-criteria filtering.
| Client | NDIS Number | Funding Type | Location | Intake Date | Status |
|---|---|---|---|---|---|
|
JT
James Thompson
ND-00001
|
430123456789 | Plan Managed | Parramatta NSW | 01 Feb 2023 | Active |
|
MG
Maria Garcia
ND-00002
|
430234567890 | NDIS Managed | Blacktown NSW | 15 Mar 2023 | Active |
|
DL
David Liu
ND-00003
|
430345678901 | Plan Managed | Liverpool NSW | 22 Apr 2023 | Active |
|
SW
Sarah Williams
ND-00004
|
430456789012 | Self Managed | Penrith NSW | 10 May 2023 | Active |
|
JA
James Anderson
ND-00005
|
430987654321 | NDIS Managed | Blacktown NSW | 01 Jun 2023 | On Hold |
| Column | Data Source | Sortable | Description |
|---|---|---|---|
| Client | first_name + last_name + client_id | Yes | Client avatar (initials), full name, and internal client ID (e.g., ND-00001) |
| NDIS Number | ndis_number | Yes | 12-digit NDIS participant number |
| Funding Type | funding_type | Yes | Plan Managed, NDIS Managed, or Self Managed |
| Location | suburb + state | Yes | Client's primary suburb and state |
| Intake Date | intake_date | Yes | Date the client was registered in the system |
| Status | status | Yes | Current client status badge |
# Python API: GET /api/clients
@app.route('/api/clients', methods=['GET'])
def list_clients():
org_id = get_current_org_id()
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 20))
search = request.args.get('search', '')
status = request.args.get('status', '')
funding_type = request.args.get('funding_type', '')
sort_by = request.args.get('sort_by', 'last_name')
sort_dir = request.args.get('sort_dir', 'asc')
query = """
SELECT c.id, c.client_id, c.first_name, c.last_name,
c.ndis_number, c.funding_type, c.suburb, c.state,
c.intake_date, c.status, c.created_at
FROM clients c
WHERE c.org_id = %s AND c.deleted_at IS NULL
"""
params = [org_id]
if search:
query += """ AND (
c.search_vector @@ plainto_tsquery('english', %s)
OR c.ndis_number ILIKE %s
)"""
params.extend([search, f'%{search}%'])
if status:
query += " AND c.status = %s"
params.append(status)
if funding_type:
query += " AND c.funding_type = %s"
params.append(funding_type)
# Sorting (whitelist allowed columns)
allowed_sorts = ['last_name', 'ndis_number', 'funding_type', 'suburb', 'intake_date', 'status']
if sort_by in allowed_sorts:
query += f" ORDER BY c.{sort_by} {sort_dir}"
# Pagination
offset = (page - 1) * per_page
query += " LIMIT %s OFFSET %s"
params.extend([per_page, offset])
clients = db.execute(query, params)
total = db.execute_scalar("SELECT COUNT(*) FROM clients WHERE org_id = %s AND deleted_at IS NULL", [org_id])
return jsonify({
'data': clients,
'pagination': {
'page': page,
'per_page': per_page,
'total': total,
'total_pages': math.ceil(total / per_page)
}
})
New clients are created through a comprehensive multi-step form that captures all essential information required for NDIS service delivery. The form includes validation rules that ensure data integrity, including NDIS number format validation, mandatory field enforcement, and duplicate detection.
| Field | Type | Required | Validation Rules | Database Column |
|---|---|---|---|---|
| First Name | Text | Yes | Min 1 char, max 100 chars, alpha + spaces only | first_name VARCHAR(100) |
| Last Name | Text | Yes | Min 1 char, max 100 chars, alpha + spaces only | last_name VARCHAR(100) |
| Date of Birth | Date | Yes | Must be a valid past date, not future | date_of_birth DATE |
| Gender | Select | No | Enum: male, female, non_binary, prefer_not_to_say | gender VARCHAR(30) |
| NDIS Number | Text | Yes | Exactly 12 digits, must start with 43, unique per org | ndis_number VARCHAR(12) UNIQUE |
| Funding Type | Select | Yes | Enum: plan_managed, ndis_managed, self_managed | funding_type VARCHAR(30) |
| Phone | Text | No | Australian phone format validation | phone VARCHAR(20) |
| No | Valid email format | email VARCHAR(255) | ||
| Street Address | Text | No | Max 255 chars | address_line1 VARCHAR(255) |
| Suburb | Text | Yes | Max 100 chars | suburb VARCHAR(100) |
| State | Select | Yes | Australian state/territory enum | state VARCHAR(3) |
| Postcode | Text | Yes | 4-digit Australian postcode | postcode VARCHAR(4) |
| Primary Coordinator | Select | No | Valid staff member with coordinator role | coordinator_id UUID FK |
| Status | Select | Yes | Enum: active, on_hold, inactive, discharged | status VARCHAR(20) |
| Notes | Textarea | No | Max 5000 chars | notes TEXT |
// NDIS Number Validation (TypeScript)
export function validateNDISNumber(value: string): ValidationResult {
// Remove spaces and hyphens
const cleaned = value.replace(/[\s-]/g, '');
if (cleaned.length !== 12) {
return { valid: false, error: 'NDIS number must be exactly 12 digits' };
}
if (!/^\d{12}$/.test(cleaned)) {
return { valid: false, error: 'NDIS number must contain only digits' };
}
if (!cleaned.startsWith('43')) {
return { valid: false, error: 'NDIS number must start with 43' };
}
return { valid: true, formatted: cleaned };
}
// Duplicate detection
export async function checkDuplicateNDIS(ndisNumber: string, orgId: string): Promise<boolean> {
const response = await fetch(`/api/clients/check-ndis?number=${ndisNumber}&org_id=${orgId}`);
const data = await response.json();
return data.exists;
}
When creating a new client, the system automatically checks for duplicate NDIS numbers within the organisation. If a match is found, the user is warned and must confirm they want to proceed. This prevents accidental duplicate records that could lead to billing and compliance issues.
The Client Profile page provides a comprehensive view of all information related to a specific client. It is organised into a tabbed interface with the following sections: Overview, Goals, Progress Notes, Funding, Care Plan, and Documents. The profile page is accessible by clicking on a client's name from the Client List or any other reference to the client throughout the platform.
| Name | Relationship | Phone | Priority | |
|---|---|---|---|---|
| Robert Thompson | Father | 0423 456 789 | robert.t@email.com | Primary |
| Emily Thompson | Sister | 0434 567 890 | emily.t@email.com | Secondary |
| Tab | Content | Key Features |
|---|---|---|
| Overview | Personal info, NDIS details, emergency contacts, recent activity | Quick edit, status badge, coordinator info |
| Goals | Active and completed goals with progress tracking | Progress bars, goal categories, review dates |
| Progress Notes | Chronological notes from shifts and interactions | Note types, linked shifts, author tracking |
| Funding | NDIS budget allocation and utilisation | Category breakdowns, utilisation bars, alerts |
| Care Plan | Active care plan with support categories | Risk assessments, review scheduling, version history |
| Documents | Uploaded files and generated documents | File categories, upload/download, expiry tracking |
The NDIS Funding Management section within the client profile provides detailed tracking of the client's NDIS plan budget, category allocations, and utilisation rates. This is critical for ensuring that service delivery stays within budget and that funding is optimally distributed across support categories.
| Category | Budget | Spent | Remaining | Utilisation |
|---|---|---|---|---|
| Core Supports | $52,000 | $38,480 | $13,520 |
|
| Capacity Building | $25,000 | $11,200 | $13,800 |
|
| Capital Supports | $8,200 | $2,750 | $5,450 |
|
| Utilisation Level | Colour Indicator | Alert Behaviour |
|---|---|---|
| 0% – 60% | Green | No alert - normal utilisation |
| 61% – 80% | Amber | Informational notice to coordinator |
| 81% – 95% | Orange | Warning alert to coordinator and admin |
| 96% – 100% | Red | Critical alert - immediate review required |
| > 100% | Red (Overspent) | System blocks new service bookings, escalates to finance |
# PHP Funding Calculation Service
class FundingCalculator
{
public function calculateUtilisation(string $clientId, string $categoryId): array
{
$plan = $this->planRepository->getActivePlan($clientId);
if (!$plan) {
throw new PlanNotFoundException("No active NDIS plan found for client {$clientId}");
}
$budget = $this->budgetRepository->getCategoryBudget($plan->id, $categoryId);
$spent = $this->invoiceRepository->getTotalSpentByCategory($plan->id, $categoryId);
$remaining = $budget->amount - $spent;
$utilisation = ($budget->amount > 0) ? ($spent / $budget->amount) * 100 : 0;
// Calculate projected utilisation at plan end
$daysElapsed = now()->diffInDays($plan->start_date);
$totalDays = $plan->end_date->diffInDays($plan->start_date);
$dailyBurnRate = ($daysElapsed > 0) ? $spent / $daysElapsed : 0;
$projectedTotal = $dailyBurnRate * $totalDays;
$projectedUtilisation = ($budget->amount > 0)
? ($projectedTotal / $budget->amount) * 100
: 0;
return [
'budget' => $budget->amount,
'spent' => $spent,
'remaining' => $remaining,
'utilisation_percent' => round($utilisation, 1),
'projected_utilisation' => round($projectedUtilisation, 1),
'daily_burn_rate' => round($dailyBurnRate, 2),
'alert_level' => $this->getAlertLevel($utilisation),
];
}
private function getAlertLevel(float $utilisation): string
{
return match(true) {
$utilisation > 100 => 'overspent',
$utilisation > 95 => 'critical',
$utilisation > 80 => 'warning',
$utilisation > 60 => 'notice',
default => 'normal',
};
}
}
Goal Tracking enables support coordinators and clinicians to define, monitor, and review client goals as part of their NDIS plan. Each goal has a measurable target, progress percentage, category, and review schedule.
| Goal | Category | Progress | Status | Review Date |
|---|---|---|---|---|
| Improve daily living independence | Core Supports |
|
Active | 15 May 2024 |
| Community participation - weekly social group | Capacity Building |
|
Active | 01 Jun 2024 |
| Mobility improvement - walking 500m | Core Supports |
|
Completed | 01 Mar 2024 |
| Employment readiness skills | Capacity Building |
|
On Hold | 01 Aug 2024 |
-- PostgreSQL: client_goals table
CREATE TABLE client_goals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
org_id UUID NOT NULL REFERENCES organisations(id),
title VARCHAR(500) NOT NULL,
description TEXT,
category VARCHAR(50) NOT NULL, -- 'core_supports', 'capacity_building', 'capital'
progress INTEGER DEFAULT 0 CHECK (progress >= 0 AND progress <= 100),
status VARCHAR(20) DEFAULT 'active', -- 'active', 'completed', 'on_hold', 'cancelled'
target_date DATE,
review_date DATE,
created_by UUID REFERENCES users(id),
completed_at TIMESTAMPTZ,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Progress Notes are chronological records of interactions, observations, and service delivery details for a client. They are typically created during or after a shift and serve as the primary documentation trail for NDIS compliance and auditing purposes.
| Note Type | Description | Required Fields | Created By |
|---|---|---|---|
| General | Standard shift or interaction notes | Content, date, author | Support Workers, Coordinators |
| Medical | Health-related observations and updates | Content, date, author, severity | Clinical Staff, Support Workers |
| Behavioural | Behavioural observations and incidents | Content, date, author, behaviour type, intensity | Support Workers, Clinicians |
| Incident | Notes linked to formal incident reports | Content, date, author, incident ID | All staff |
| Goal Update | Progress updates on specific goals | Content, date, author, goal ID, progress change | Coordinators, Clinicians |
| Family Communication | Records of communication with family/guardians | Content, date, author, contact person | Coordinators |
Care Plans define the comprehensive support framework for each client, including support categories, service schedules, risk assessments, and review timelines. Care plans are versioned documents that are reviewed and updated at regular intervals in line with NDIS plan review cycles.
| Component | Description | Review Frequency |
|---|---|---|
| Support Categories | The NDIS support categories covered (Core, Capacity Building, Capital) with specific line items | Quarterly |
| Service Schedule | Regular service delivery schedule including days, times, and service types | Monthly |
| Risk Assessment | Identified risks (health, behavioural, environmental) with mitigation strategies | Quarterly or on change |
| Goals & Outcomes | Linked goals from the Goals module with expected outcomes | Quarterly |
| Communication Preferences | How the client prefers to communicate, language needs, accessibility requirements | Annually |
| Emergency Protocols | Specific emergency response procedures for this client | Annually |
| Medication Management | Current medications, administration instructions, pharmacy details | Monthly or on change |
| Dietary Requirements | Allergies, dietary restrictions, meal preferences | Annually or on change |
Every modification to a care plan creates a new version. The system maintains a complete version history for audit purposes. Previous versions are read-only but can be viewed for comparison. The versioning is implemented using a combination of PostgreSQL row versioning and a PHP service that manages version diffs.
-- PostgreSQL: care_plan_versions table
CREATE TABLE care_plan_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
care_plan_id UUID NOT NULL REFERENCES care_plans(id),
version_number INTEGER NOT NULL,
content JSONB NOT NULL,
change_summary TEXT,
created_by UUID REFERENCES users(id),
approved_by UUID REFERENCES users(id),
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(care_plan_id, version_number)
);
Client status determines the operational state of a client within NDSS CRM and affects what actions can be performed on their record.
| Status | Badge | Description | Allowed Actions |
|---|---|---|---|
| Active | Active | Client is currently receiving services. All modules are fully functional. | All: shift booking, invoicing, goal tracking, notes |
| On Hold | On Hold | Temporarily paused. No new shifts can be booked but existing scheduled shifts remain. | View only, notes, goal updates. No new shifts or invoices. |
| Inactive | Inactive | No longer actively receiving services but record is retained for reference. | View only. No operational actions. |
| Discharged | Discharged | Formally discharged from the service. Record is archived. | View only. Requires admin to reactivate. |
| From | To | Required By | Conditions |
|---|---|---|---|
| Active | On Hold | Coordinator or Admin | No active shifts in next 24 hours, or admin override |
| Active | Inactive | Admin only | All pending invoices resolved, no upcoming shifts |
| Active | Discharged | Admin only | Discharge form completed, all billing finalised |
| On Hold | Active | Coordinator or Admin | None (immediate) |
| On Hold | Inactive | Admin only | On hold for more than 90 days, or admin decision |
| Inactive | Active | Admin only | Valid NDIS plan required, coordinator assigned |
| Discharged | Active | Super Admin only | Requires new intake process and approval |
The Client Management module supports comprehensive search and filtering capabilities to help users quickly locate client records. Searching leverages PostgreSQL full-text search with GIN indexes for fast, relevant results.
| Search Method | Examples | Implementation |
|---|---|---|
| Name Search | "James", "Thompson", "James Thompson" | Full-text search on first_name, last_name |
| NDIS Number | "430123456789", "4301234" | ILIKE prefix match on ndis_number |
| Client ID | "ND-00001" | Exact match on client_id |
| Suburb | "Parramatta", "Blacktown" | Full-text search on suburb |
| Phone | "0412345678" | Normalised phone number match |
| "james.t@email.com" | ILIKE match on email |
| Filter | Options | Default | Multi-select |
|---|---|---|---|
| Status | Active, On Hold, Inactive, Discharged | All | Yes |
| Funding Type | Plan Managed, NDIS Managed, Self Managed | All | Yes |
| Coordinator | List of active coordinators | All | Yes |
| Location (State) | NSW, VIC, QLD, WA, SA, TAS, ACT, NT | All | Yes |
| Intake Date Range | Custom date range picker | All time | N/A |
| Plan Expiry | Expiring in 30/60/90 days, Expired | All | No |
-- PostgreSQL: Full-text search configuration for clients
ALTER TABLE clients ADD COLUMN search_vector tsvector;
CREATE FUNCTION update_client_search_vector() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', COALESCE(NEW.first_name, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.last_name, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(NEW.ndis_number, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(NEW.suburb, '')), 'C') ||
setweight(to_tsvector('english', COALESCE(NEW.email, '')), 'D') ||
setweight(to_tsvector('english', COALESCE(NEW.notes, '')), 'D');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER client_search_vector_update
BEFORE INSERT OR UPDATE ON clients
FOR EACH ROW EXECUTE FUNCTION update_client_search_vector();
CREATE INDEX idx_clients_search ON clients USING GIN(search_vector);
The full-text search index is updated automatically via the PostgreSQL trigger shown above. For organisations with more than 10,000 clients, consider implementing a dedicated search service using Elasticsearch or a similar tool, with the Python microservice handling the indexing pipeline. The GIN index provides excellent performance for most use cases up to that threshold.