Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions crm/api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
Copyright 2009-2023 Edward L. Platt <ed@elplatt.com>

This file is part of the Seltzer CRM Project
autocomplete.php - Provides data for autocomplete elements

Seltzer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

Seltzer is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Seltzer. If not, see <http://www.gnu.org/licenses/>.
*/

// Save path of directory containing index.php
$crm_root = dirname(__FILE__);

// Bootstrap the site
include('include/crm.inc.php');

$handler = $_GET['endpoint'] . '_api';
if (function_exists($handler)) {
$params = $_GET;
print json_encode(call_user_func($handler, $params), JSON_NUMERIC_CHECK);
}
131 changes: 131 additions & 0 deletions crm/modules/member/charts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Your JSON endpoints for Membership income and Expected membership income data
const membershipIncomeEndpoint = 'service.php?endpoint=monthly_payments';
const expectedMembershipIncomeEndpoint = 'service.php?endpoint=monthly_payments_due';

// Fetch data from the JSON endpoints
async function fetchData() {
const membershipResponse = await fetch(membershipIncomeEndpoint);
const expectedMembershipResponse = await fetch(expectedMembershipIncomeEndpoint);

const membershipData = await membershipResponse.json();
const expectedMembershipData = await expectedMembershipResponse.json();

return { membershipData, expectedMembershipData };
}

// Process and format the data for Chart.js
function prepareData(data) {
const labels = data.map(item => `${item.year}-${item.month}`);
const membershipIncome = data.map(item => item.amount);
return { labels, membershipIncome };
}

function calculateCumulativeIncome(monthlyIncome) {
for (let i = 1; i < monthlyIncome.length; i++) {
monthlyIncome[i]['amount'] += monthlyIncome[i-1]['amount'];
}
return monthlyIncome;
}

// Create the line chart
async function createLineChart(container, chart_title, cumulative = false) {
const data = await fetchData();
if (cumulative == true) {
preparedMembershipData = prepareData(calculateCumulativeIncome(data.membershipData));
preparedExpectedMembershipData = prepareData(calculateCumulativeIncome(data.expectedMembershipData));
} else {
preparedMembershipData = prepareData(data.membershipData);
preparedExpectedMembershipData = prepareData(data.expectedMembershipData);
}
const ctx = document.getElementById(container).getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: preparedMembershipData.labels,
datasets: [
{
label: 'Membership Income',
data: preparedMembershipData.membershipIncome,
borderColor: '#3498db', // Blue
fill: false,
},
{
label: 'Expected Membership Income',
data: preparedExpectedMembershipData.membershipIncome,
borderColor: '#2ecc71', // Green
fill: false,
},
],
},
options: {
responsive: true,
scales: {
x: {
title: {
display: true,
text: 'Date',
},
},
y: {
title: {
display: true,
text: 'Amount',
},
},
},
plugins: {
title: {
display: true,
text: chart_title,
}
}
},
});
}

async function plan_pie_chart() {
const plan_data = await (await fetch('api.php?endpoint=plan_distribution')).json();
const CHART_COLORS = {
yellow: 'rgb(255, 205, 86)',
green: 'rgb(75, 192, 192)',
blue: 'rgb(54, 162, 235)',
purple: 'rgb(153, 102, 255)',
grey: 'rgb(201, 203, 207)',
red: 'rgb(255, 99, 132)',
orange: 'rgb(255, 159, 64)',
};
const data = {
labels: plan_data.map(item => item.name),
datasets: [
{
label: 'Dataset 1',
data: plan_data.map(item => item.count),
backgroundColor: Object.values(CHART_COLORS),
}
]
};
console.log(data)
const config = {
type: 'pie',
data: data,
options: {
responsive: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: 'Active Plans'
}
}
},
};
const ctx = document.getElementById('plan-distribution').getContext('2d');
new Chart(ctx, config);
}

// Call the function to create the line chart
createLineChart('memberships-monthly', 'Memberships');
createLineChart('memberships-monthly-cumulative', 'Cumulative Memberships', true);
plan_pie_chart();
121 changes: 12 additions & 109 deletions crm/modules/member/theme.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,115 +43,18 @@ function theme_member_email_report ($opts) {
* @return The themed html for a membership report.
*/
function theme_member_membership_report () {
$json = member_statistics();
$output = <<<EOF
<h2>Membership Report</h2>
<svg id="membership-report" width="960" height="500">
</svg>
<script type="text/javascript">
// Calculate stacking for data
var layers = $json;
var stack = d3.layout.stack()
.values(function (d) { return d.values; });
layers = stack(layers);
var n = layers.length;
var m = layers[0].values.length;

// Calculate geometry
var chartWidth = 960,
chartHeight = 500;
var padding = { left:50, bottom:100, top:25, right:50 };
var width = chartWidth - padding.left - padding.right;
var height = chartHeight - padding.bottom - padding.top;

// Create scales
var x = d3.scale.linear()
.domain([d3.min(layers, function(layer) { return d3.min(layer.values, function(d) { return d.x; }); }), d3.max(layers, function(layer) { return d3.max(layer.values, function(d) { return d.x; }); })])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(layers, function(layer) { return d3.max(layer.values, function(d) { return d.y0 + d.y; }); })])
.range([height, 0]);
var color = d3.scale.linear()
.range(["#aa3", "#aaf"])
.domain([0, 1]);

var colors = [];
//var roff = Math.round(Math.random()*15);
//var goff = Math.round(Math.random()*15);
//var boff = Math.round(Math.random()*15);
var roff = 12, goff = 7, boff = 8;
console.log([roff, goff, boff]);
for (var i = 0; i < layers.length; i++) {
var r = ((i+roff) * 3) % 16;
var g = ((i+goff) * 5) % 16;
var b = ((i+boff) * 7) % 16;
colors[i] = '#' + r.toString(16) + g.toString(16) + b.toString(16);
}

// Set up the svg element
var svg = d3.select("#membership-report")
.attr("width", chartWidth)
.attr("height", chartHeight);

// Define axes
var yaxis = d3.svg.axis().orient('left').scale(y);
var xlabel = d3.scale.ordinal()
.domain(layers[0].values.map(function (d) { return d.label; }))
.rangePoints([0, width]);
var xaxis = d3.svg.axis().orient('bottom').scale(xlabel);

// Draw lines
var chart = svg.append('g')
.attr('transform', 'translate(' + padding.left + ',' + padding.top + ')');
chart.selectAll('.rule')
.data(y.ticks(yaxis.ticks()))
.enter()
.append('line')
.attr('class', 'rule')
.attr('x1', '0').attr('x2', width)
.attr('y1', '0').attr('y2', '0')
.attr('transform', function(d) { return 'translate(0,' + y(d) + ')'; })
.style('stroke', '#eee');

// Draw the data
var area = d3.svg.area()
.x(function(d) { return x(d.x); })
.y0(function(d) { return y(d.y0); })
.y1(function(d) { return y(d.y0 + d.y); });
chart.selectAll("path")
.data(layers)
.enter().append("path")
.attr('width', width)
.attr("d", function (d) { return area(d.values); })
.style("fill", function(d,i) { return colors[i]; });

// Draw the axes
chart.append('g').attr('id', 'yaxis').attr('class', 'axis').call(yaxis).attr('transform', 'translate(-0.5, 0.5)');
yaxis.orient('right');
chart.append('g').attr('id', 'yaxis').attr('class', 'axis').call(yaxis).attr('transform', 'translate(' + (width-0.5) + ',0.5)');
chart.append('g').attr('id', 'xaxis').attr('class', 'axis').call(xaxis)
.attr('transform', 'translate(-0.5,' + (height+0.5) + ')')
.selectAll('text')
.style('text-anchor', 'end')
.attr('dy', '-.35em')
.attr('dx', '-9')
.attr('transform', 'rotate(-90)');
d3.selectAll('.axis path').attr('fill', 'none').attr('stroke', 'black');

// Draw a legend
var legend = chart.append('g').attr('id', 'legend')
.selectAll('g').data(layers)
.enter()
.append('g')
.attr('transform', function(d,i) { return 'translate(10,' + ((layers.length - i - 1)*22) + ')'; });
legend.append('rect')
.attr('width', '20').attr('height', '20')
.style("fill", function(d,i) { return colors[i]; });
legend.append('text')
.text(function (d) { return d.name; })
.attr('transform', 'translate(25, 15)');
</script>
EOF;
$output = <<<HTML
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<h2>Membership Report</h2>
<div style="width: 100%; margin: 0 auto;">
<canvas id="memberships-monthly"></canvas>
<canvas id="memberships-monthly-cumulative"></canvas>
</div>
<div style="width: 50%; margin: 0 auto;">
<canvas id="plan-distribution"></canvas>
</div>
<script src="modules/member/charts.js"></script>
HTML;
return $output;
}

Expand Down
73 changes: 73 additions & 0 deletions crm/modules/payment/payment.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,79 @@ function command_payment_filter () {
return crm_url('payments') . $query;
}


function monthly_payments_service($options) {
global $db_connect;

$sql = "
SELECT MONTH(date) AS month, YEAR(date) AS year, SUM(value)/100 AS amount
FROM payment
WHERE description NOT LIKE 'Dues%' GROUP BY MONTH(date), YEAR(date)
ORDER BY year, month;
";
$res = mysqli_query($db_connect, $sql);
if (!$res) crm_error(mysqli_error($res));
while ($row = mysqli_fetch_assoc($res)) {
$data[] = $row;
}
return $data;
}

function monthly_payments_due_service($options) {
global $db_connect;

$sql = "
SELECT MONTH(date) AS month, YEAR(date) AS year, SUM(value)/-100 AS amount
FROM payment
WHERE description LIKE 'Dues%' GROUP BY MONTH(date), YEAR(date)
ORDER BY year, month;
";
$res = mysqli_query($db_connect, $sql);
if (!$res) crm_error(mysqli_error($res));
while ($row = mysqli_fetch_assoc($res)) {
$data[] = $row;
}
return $data;
}

function monthly_cumulative_income_api($options) {
global $db_connect;

$sql = "
SELECT DISTINCT
YEAR(date) AS year,
MONTH(date) AS month,
SUM(value) OVER (ORDER BY YEAR(date) ASC, MONTH(date) ASC) AS cumulative_income
FROM payment
WHERE description LIKE 'Dues%'
ORDER BY year, month;
";
$res = mysqli_query($db_connect, $sql);
if (!$res) crm_error(mysqli_error($res));
while ($row = mysqli_fetch_assoc($res)) {
$data[] = $row;
}
return $data;
}

function plan_distribution_api($options) {
global $db_connect;

$sql = "
SELECT
plan.name,
COUNT(*) as count
FROM membership JOIN plan USING(pid)
WHERE end is NULL GROUP BY pid
";
$res = mysqli_query($db_connect, $sql);
if (!$res) crm_error(mysqli_error($res));
while ($row = mysqli_fetch_assoc($res)) {
$data[] = $row;
}
return $data;
}

function get_last_payment($cid) {
global $db_connect;
$sql = "
Expand Down
Loading