From 59c1620e896aa895cb5ac78da7fbd886471c5921 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Sun, 24 Feb 2013 05:30:27 +0100 Subject: [PATCH] Currency conversion and formatting, a real dashboard, and statistics generation --- public_html/classes/campaign.php | 92 ++++++++++++++++++-- public_html/classes/currency.php | 91 +++++++++++++++++++ public_html/classes/exchangerate.php | 56 ++++++++++++ public_html/classes/logentry.php | 2 + public_html/classes/paymentmethod.php | 53 ++++++++++++ public_html/modules/dashboard.php | 50 ++++++++++- public_html/modules/landing.php | 2 + public_html/static/css/style.css | 120 ++++++++++++++++++++++++++ public_html/templates/dashboard.tpl | 48 ++++++++++- 9 files changed, 503 insertions(+), 11 deletions(-) create mode 100644 public_html/classes/currency.php create mode 100644 public_html/classes/exchangerate.php create mode 100644 public_html/classes/paymentmethod.php diff --git a/public_html/classes/campaign.php b/public_html/classes/campaign.php index a806d82..31266d0 100644 --- a/public_html/classes/campaign.php +++ b/public_html/classes/campaign.php @@ -21,17 +21,25 @@ class Campaign extends CPHPDatabaseRecordClass public $prototype = array( 'string' => array( - 'Name' => "Name", - 'UrlName' => "UrlName" + 'Name' => "Name", + 'UrlName' => "UrlName" ), 'numeric' => array( - 'OwnerId' => "UserId" + 'OwnerId' => "UserId", + 'DonationRate' => "DonationRate", + 'SubscriberCount' => "SubscriberCount", + 'MonthlyTotal' => "TotalMonthlyDonations", + 'MonthlyProjection' => "ProjectedMonthlyDonations" ), 'boolean' => array( - 'AllowOneTime' => "AllowOneTime" + 'AllowOneTime' => "AllowOneTime", + 'HaveData' => "HaveData" + ), + 'timestamp' => array( + 'LastStatisticsUpdate' => "LastStatisticsUpdate" ), 'user' => array( - 'Owner' => "Owner" + 'Owner' => "Owner" ) ); @@ -52,4 +60,78 @@ class Campaign extends CPHPDatabaseRecordClass { return self::CreateFromQuery("SELECT * FROM campaigns WHERE `UrlName` = :UrlName", array(':UrlName' => $urlname), 0, true); } + + public function UpdateStatistics() + { + global $database; + + if($this->sLastStatisticsUpdate < time() - (60 * 5)) + { + /* Update subscriber count */ + if($result = $database->CachedQuery("SELECT COUNT(*) FROM subscriptions WHERE `CampaignId` = :CampaignId AND `Confirmed` = 1", array(":CampaignId" => $this->sId))) + { + $this->uSubscriberCount = $result->data[0]["COUNT(*)"]; + } + + /* Update total monthly donations */ + try + { + $sSubscriptions = Subscription::CreateFromQuery("SELECT * FROM subscriptions WHERE `CampaignId` = :CampaignId AND `Confirmed` = 1", array(":CampaignId" => $this->sId)); + $sTotalDonations = 0; + + foreach($sSubscriptions as $sSubscription) + { + $sTotalDonations += Currency::Convert("usd", $sSubscription->sCurrency, $sSubscription->sAmount); + } + + $this->uMonthlyTotal = $sTotalDonations; + } + catch (NotFoundException $e) + { + $this->uMonthlyTotal = 0; + } + + /* Update donation rate */ + try + { + $sDonationsAsked = LogEntry::CreateFromQuery("SELECT * FROM log_entries WHERE `CampaignId` = :CampaignId AND `Type` = :Type AND `Date` > DATE_SUB(NOW(), INTERVAL 1 MONTH)", + array(":CampaignId" => $this->sId, ":Type" => LogEntry::DONATION_ASKED)); + + $have_data = true; + } + catch (NotFoundException $e) + { + /* We don't have any data to work from yet. */ + $have_data = false; + } + + if($have_data) + { + try + { + $sDonationsMade = LogEntry::CreateFromQuery("SELECT * FROM log_entries WHERE `CampaignId` = :CampaignId AND `Type` = :Type AND `Date` > DATE_SUB(NOW(), INTERVAL 1 MONTH)", + array(":CampaignId" => $this->sId, ":Type" => LogEntry::DONATION_MADE)); + + $this->uDonationRate = (count($sDonationsMade) / count($sDonationsAsked)) * 100; + $this->uHaveData = true; + } + catch (NotFoundException $e) + { + $this->uDonationRate = 0; + $this->uHaveData = false; + } + } + else + { + $this->uDonationRate = 100; + $this->uHaveData = false; + } + + /* Update projected monthly donations */ + $this->uMonthlyProjection = $this->uMonthlyTotal * ($this->uDonationRate / 100); + + $this->uLastStatisticsUpdate = time(); + $this->InsertIntoDatabase(); + } + } } diff --git a/public_html/classes/currency.php b/public_html/classes/currency.php new file mode 100644 index 0000000..ecca03d --- /dev/null +++ b/public_html/classes/currency.php @@ -0,0 +1,91 @@ + $original_currency), 300, true); + } + catch (NotFoundException $e) + { + throw new CurrencyConversionException("The specified origin currency is not a valid currency code."); + } + + $sUsdRate = $amount * $sOriginRate->sFromRate; + + if($target_currency === "USD") + { + return $sUsdRate; + } + else + { + try + { + $sTargetRate = ExchangeRate::CreateFromQuery("SELECT * FROM exchange_rates WHERE `Code` = :Code", array(":Code" => $target_currency), 300, true); + } + catch (NotFoundException $e) + { + throw new CurrencyConversionException("The specified target currency is not a valid currency code."); + } + + $sResult = $sUsdRate * $sTargetRate->sToRate; + + return $sResult; + } + } + + public static function Format($currency, $amount, $precision = 2) + { + try + { + $sExchangeRate = ExchangeRate::CreateFromQuery("SELECT * FROM exchange_rates WHERE `Code` = :Code", array(":Code" => $currency), 30, true); + } + catch (NotFoundException $e) + { + throw new CurrencyFormattingException("The specified currency is not a valid currency code."); + } + + if($sExchangeRate->sSymbol == "") + { + return "{$sExchangeRate->sCurrencyCode} " . number_format($amount, $precision); + } + else + { + return "{$sExchangeRate->sSymbol}" . number_format($amount, $precision); + } + } + + public static function UpdateRates() + { + global $cphp_config; + + $json = json_decode(file_get_contents("http://openexchangerates.org/api/latest.json?app_id={$cphp_config->openexchangerates->app_id}"), true); + $rates = $json["rates"]; + + foreach($rates as $currency => $rate) + { + ExchangeRate::Update($currency, $rate); + } + } +} diff --git a/public_html/classes/exchangerate.php b/public_html/classes/exchangerate.php new file mode 100644 index 0000000..5a08cd1 --- /dev/null +++ b/public_html/classes/exchangerate.php @@ -0,0 +1,56 @@ + array( + 'Name' => "Name", + 'CurrencyCode' => "Code", + 'Symbol' => "Symbol" + ), + 'numeric' => array( + 'FromRate' => "From", + 'ToRate' => "To" + ), + 'timestamp' => array( + 'UpdateDate' => "Updated" + ) + ); + + public static function Update($code, $to) + { + $from = 1 / $to; + + try + { + $sRate = ExchangeRate::CreateFromQuery("SELECT * FROM exchange_rates WHERE `Code` = :Code", array(":Code" => $code), 0, true); + } + catch(NotFoundException $e) + { + $sRate = new ExchangeRate(0); + $sRate->uCurrencyCode = $code; + } + + $sRate->uToRate = $to; + $sRate->uFromRate = $from; + $sRate->uUpdateDate = time(); + $sRate->InsertIntoDatabase(); + } +} diff --git a/public_html/classes/logentry.php b/public_html/classes/logentry.php index 9ac2d1e..4b4e9ce 100644 --- a/public_html/classes/logentry.php +++ b/public_html/classes/logentry.php @@ -39,4 +39,6 @@ class LogEntry extends CPHPDatabaseRecordClass const PAGELOAD = 1; const SUBSCRIPTION = 2; + const DONATION_ASKED = 3; + const DONATION_MADE = 4; } diff --git a/public_html/classes/paymentmethod.php b/public_html/classes/paymentmethod.php new file mode 100644 index 0000000..dd063db --- /dev/null +++ b/public_html/classes/paymentmethod.php @@ -0,0 +1,53 @@ + array( + 'Address' => "Address" + ), + 'numeric' => array( + 'Type' => "Type", + 'CampaignId' => "CampaignId" + ), + 'campaign' => array( + 'Campaign' => "Campaign" + ) + ); + + const PAYPAL = 1; + const BITCOIN = 2; + const IBAN = 3; + + public function GetLogo() + { + switch($this->sType) + { + case PaymentMethod::PAYPAL: + return array("image" => "/static/images/paypal.png", "text" => "PayPal"); + case PaymentMethod::BITCOIN: + return array("image" => "/static/images/bitcoin.png", "text" => "Bitcoin"); + case PaymentMethod::IBAN: + return array("text" => "IBAN"); + default: + return array("text" => "Unknown"); + } + } +} diff --git a/public_html/modules/dashboard.php b/public_html/modules/dashboard.php index 2e60b49..ae36d42 100644 --- a/public_html/modules/dashboard.php +++ b/public_html/modules/dashboard.php @@ -15,12 +15,49 @@ if(!isset($_APP)) { die("Unauthorized."); } $sCampaigns = array(); +$sPercentages = array(); +$sTotals = array(); +$sProjections = array(); +$sSubscribers = array(); + try { foreach(Campaign::CreateFromQuery("SELECT * FROM campaigns WHERE `OwnerId` = :UserId", array(":UserId" => $sCurrentUser->sId)) as $sCampaign) { + $sCampaign->UpdateStatistics(); + $sPaymentMethods = array(); + + try + { + foreach(PaymentMethod::CreateFromQuery("SELECT * FROM payment_methods WHERE `CampaignId` = :CampaignId", + array(":CampaignId" => $sCampaign->sId)) as $sPaymentMethod) + { + $sPaymentMethods[] = $sPaymentMethod->GetLogo(); + } + } + catch (NotFoundException $e) + { + /* No payment methods...? */ + } + + if($sCampaign->sHaveData) + { + $sPercentages[] = $sCampaign->sDonationRate; + $sTotals[] = $sCampaign->sMonthlyTotal; + $sProjections[] = $sCampaign->sMonthlyProjection; + } + + $sSubscribers[] = $sCampaign->sSubscriberCount; + $sCampaigns[] = array( - "name" => $sCampaign->sName + "name" => $sCampaign->sName, + "subscribers" => number_format($sCampaign->sSubscriberCount, 0), + "rate" => number_format($sCampaign->sDonationRate, 2), + "total" => Currency::Format("usd", $sCampaign->sMonthlyTotal), + "projection" => Currency::Format("usd", $sCampaign->sMonthlyProjection), + "one-off" => $sCampaign->sAllowOneTime, + "payment-methods" => $sPaymentMethods, + "have-data" => $sCampaign->sHaveData ); } } @@ -29,7 +66,16 @@ catch (NotFoundException $e) /* pass */ } +$sPercentages = (empty($sPercentages)) ? array(0) : $sPercentages; +$sTotals = (empty($sTotals)) ? array(0) : $sTotals; +$sProjections = (empty($sProjections)) ? array(0) : $sProjections; +$sSubscribers = (empty($sSubscribers)) ? array(0) : $sSubscribers; + $sPageTitle = "Dashboard"; $sPageContents = NewTemplater::Render("dashboard", $locale->strings, array( - "campaigns" => $sCampaigns + "campaigns" => $sCampaigns, + "total-rate" => number_format(array_sum($sPercentages) / count($sPercentages), 2), + "total-subscribers" => number_format(array_sum($sSubscribers), 0), + "total-total" => Currency::Format("usd", array_sum($sTotals)), + "total-projection" => Currency::Format("usd", array_sum($sProjections)) )); diff --git a/public_html/modules/landing.php b/public_html/modules/landing.php index 2fc7bb9..0a4d551 100644 --- a/public_html/modules/landing.php +++ b/public_html/modules/landing.php @@ -24,6 +24,8 @@ catch (NotFoundException $e) return; } +$sCampaign->UpdateStatistics(); + $sLogEntry = new LogEntry(0); $sLogEntry->uType = LogEntry::PAGELOAD; $sLogEntry->uIp = $_SERVER['REMOTE_ADDR']; diff --git a/public_html/static/css/style.css b/public_html/static/css/style.css index 0a78757..4599fa5 100644 --- a/public_html/static/css/style.css +++ b/public_html/static/css/style.css @@ -269,6 +269,93 @@ form button:active margin-bottom: 13px; } +/************************************** + * TABLES * + **************************************/ + +table +{ + border-collapse: collapse; + width: 100%; +} + +table, td +{ + border: 1px solid #688A3C; +} + +th, td +{ + padding: 7px 10px; +} + +td +{ + color: #292929; +} + +th +{ + text-align: left; + font-size: 17px; + background-color: #688A3C; + border: 1px solid #5A6B46; + color: #FDFFFD; + text-shadow: 0px 1px 0px #1A5200; + -webkit-text-shadow: 0px 1px 0px #1A5200; + -moz-text-shadow: 0px 1px 0px #1A5200; + -o-text-shadow: 0px 1px 0px #1A5200; + -ms-text-shadow: 0px 1px 0px #1A5200; +} + +th.icon +{ + padding: 4px; + text-align: center; +} + +td.numeric +{ + text-align: right; +} + +td.name, td.total +{ + font-weight: bold; +} + +td.name +{ + color: #203A00; +} + +td.total +{ + color: black; +} + +tr.total td +{ + border-top: 2px solid #030600; + border-right: 1px solid #D3E8B7; + border-left: 1px solid #D3E8B7; +} + +tr.total td:nth-child(1) +{ + border-left: 1px solid #688A3C; +} + +tr.total td:nth-last-child(1) +{ + border-right: 1px solid #688A3C; +} + +td.meta +{ + font-style: italic; +} + /************************************** * NOTIFICATIONS * **************************************/ @@ -291,6 +378,39 @@ form button:active background-color: #F8FFF7; } +/************************************** + * LOGOS * + **************************************/ + +.logo.thumb +{ + margin-right: 11px; +} + +img.logo.thumb +{ + vertical-align: middle; + height: 20px; +} + +div.logo.thumb +{ + font-size: 18px; + font-weight: bold; + font-style: italic; + color: #1F2E0B; + display: inline-block; +} + +/************************************** + * DASHBOARD * + **************************************/ + +td.payment-methods +{ + max-width: 240px; +} + /************************************** * LANDING * **************************************/ diff --git a/public_html/templates/dashboard.tpl b/public_html/templates/dashboard.tpl index 25506ee..acc6be8 100644 --- a/public_html/templates/dashboard.tpl +++ b/public_html/templates/dashboard.tpl @@ -1,3 +1,5 @@ +

Dashboard

+ {%if isempty|notices == false} {%foreach notice in notices}
@@ -9,14 +11,52 @@ - + + + + + {%foreach campaign in campaigns} - - - + + + + + + + {%/foreach} + + + + + + + +
NameCampaign typeTypeAmount of subscribersPercentage of subscribed donations actually made in the past monthTotal amount of subscribed donations per monthEstimate of real donations per month, based on donation rate Payment methods
{%?campaign[name]}{%?campaign[name]} + {%if campaign[one-off] == false} + Recurring + {%else} + Recurring & One-off + {%/if} + {%?campaign[subscribers]} + {%if campaign[have-data] == true} + {%?campaign[rate]}% + {%else} + - + {%/if} + {%?campaign[total]}{%?campaign[projection]} + {%foreach method in campaign[payment-methods]} + {%if isempty|method[image] == false} + + {%else} + + {%/if} + {%/foreach} +
Total{%?total-subscribers}{%?total-rate}%{%?total-total}{%?total-projection} + +