From 9a73e08b89aa0957827f2bc1f4a7cb09fdc40f5e Mon Sep 17 00:00:00 2001 From: Saleem Hadad Date: Sat, 15 Jan 2022 18:08:34 +0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20new=20reports=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Domain/Metrics/RelationTrendMetric.php | 21 ++ app/Domain/Metrics/TrendMetric.php | 8 + app/Domain/Ranges/LastTwelveMonths.php | 16 ++ app/GraphQL/Queries/TotalExpensesTrend.php | 42 ++++ app/GraphQL/Queries/TotalIncomeTrend.php | 42 ++++ app/GraphQL/Queries/TotalPerBrandTrend.php | 49 ++++ app/GraphQL/Queries/TotalPerCategoryTrend.php | 49 ++++ config/finance.php | 13 +- graphql/schema.graphql | 6 + public/css/app.css | 12 + public/js/app.js | 233 +++++++++++++++++- resources/js/Components/TrendMetric.js | 150 +++++++++++ resources/js/Components/index.js | 2 + tests/Unit/LastTwelveMonthsTest.php | 26 ++ tests/Unit/TrendMetricTest.php | 30 +++ 15 files changed, 693 insertions(+), 6 deletions(-) create mode 100644 app/Domain/Metrics/RelationTrendMetric.php create mode 100644 app/Domain/Metrics/TrendMetric.php create mode 100644 app/Domain/Ranges/LastTwelveMonths.php create mode 100644 app/GraphQL/Queries/TotalExpensesTrend.php create mode 100644 app/GraphQL/Queries/TotalIncomeTrend.php create mode 100644 app/GraphQL/Queries/TotalPerBrandTrend.php create mode 100644 app/GraphQL/Queries/TotalPerCategoryTrend.php create mode 100644 resources/js/Components/TrendMetric.js create mode 100644 tests/Unit/LastTwelveMonthsTest.php create mode 100644 tests/Unit/TrendMetricTest.php diff --git a/app/Domain/Metrics/RelationTrendMetric.php b/app/Domain/Metrics/RelationTrendMetric.php new file mode 100644 index 0000000..714ae05 --- /dev/null +++ b/app/Domain/Metrics/RelationTrendMetric.php @@ -0,0 +1,21 @@ + [ + 'graphql_query' => $this->relationGraphqlQuery, + 'display_using' => $this->relationDisplayUsing, + 'foreign_key' => $this->relationForeignKey, + ] + ]); + } +} \ No newline at end of file diff --git a/app/Domain/Metrics/TrendMetric.php b/app/Domain/Metrics/TrendMetric.php new file mode 100644 index 0000000..1dc1684 --- /dev/null +++ b/app/Domain/Metrics/TrendMetric.php @@ -0,0 +1,8 @@ +subMonths(12)->format("Y-m-d"); + } + + public function end() + { + return now()->format("Y-m-d"); + } +} \ No newline at end of file diff --git a/app/GraphQL/Queries/TotalExpensesTrend.php b/app/GraphQL/Queries/TotalExpensesTrend.php new file mode 100644 index 0000000..dde2faa --- /dev/null +++ b/app/GraphQL/Queries/TotalExpensesTrend.php @@ -0,0 +1,42 @@ + $args + */ + public function __invoke($_, array $args) + { + $rangeData = app('findRangeByKey', ["key" => $args['range']]); + + $query = Transaction::query() + ->expenses() + ->select(DB::raw("date_format(created_at, '%Y-%M') as label, SUM(transactions.amount) as value")) + ->groupBy(DB::raw("label")); + + if($rangeData) { + $query->whereBetween('transactions.created_at', [$rangeData->start(), $rangeData->end()]); + } + + return $query->get(); + } +} diff --git a/app/GraphQL/Queries/TotalIncomeTrend.php b/app/GraphQL/Queries/TotalIncomeTrend.php new file mode 100644 index 0000000..2e02d02 --- /dev/null +++ b/app/GraphQL/Queries/TotalIncomeTrend.php @@ -0,0 +1,42 @@ + $args + */ + public function __invoke($_, array $args) + { + $rangeData = app('findRangeByKey', ["key" => $args['range']]); + + $query = Transaction::query() + ->income() + ->select(DB::raw("date_format(created_at, '%Y-%M') as label, SUM(transactions.amount) as value")) + ->groupBy(DB::raw("label")); + + if($rangeData) { + $query->whereBetween('transactions.created_at', [$rangeData->start(), $rangeData->end()]); + } + + return $query->get(); + } +} diff --git a/app/GraphQL/Queries/TotalPerBrandTrend.php b/app/GraphQL/Queries/TotalPerBrandTrend.php new file mode 100644 index 0000000..c543692 --- /dev/null +++ b/app/GraphQL/Queries/TotalPerBrandTrend.php @@ -0,0 +1,49 @@ + $args + */ + public function __invoke($_, array $args) + { + $rangeData = app('findRangeByKey', ["key" => $args['range']]); + $brandId = $args['id']; + + $query = Transaction::query() + ->whereHas('brand', function ($query) use($brandId) { + return $query->where('id', $brandId); + }) + ->select(DB::raw("date_format(created_at, '%Y-%M') as label, SUM(transactions.amount) as value")) + ->groupBy(DB::raw("label")); + + if($rangeData) { + $query->whereBetween('transactions.created_at', [$rangeData->start(), $rangeData->end()]); + } + + return $query->get(); + } +} diff --git a/app/GraphQL/Queries/TotalPerCategoryTrend.php b/app/GraphQL/Queries/TotalPerCategoryTrend.php new file mode 100644 index 0000000..7d03738 --- /dev/null +++ b/app/GraphQL/Queries/TotalPerCategoryTrend.php @@ -0,0 +1,49 @@ + $args + */ + public function __invoke($_, array $args) + { + $rangeData = app('findRangeByKey', ["key" => $args['range']]); + $categoryId = $args['id']; + + $query = Transaction::query() + ->whereHas('brand.category', function ($query) use($categoryId) { + return $query->where('id', $categoryId); + }) + ->select(DB::raw("date_format(created_at, '%Y-%M') as label, SUM(transactions.amount) as value")) + ->groupBy(DB::raw("label")); + + if($rangeData) { + $query->whereBetween('transactions.created_at', [$rangeData->start(), $rangeData->end()]); + } + + return $query->get(); + } +} diff --git a/config/finance.php b/config/finance.php index 1effff1..3e63155 100644 --- a/config/finance.php +++ b/config/finance.php @@ -5,6 +5,10 @@ use App\GraphQL\Queries\TotalPerBrand; use App\GraphQL\Queries\IncomePerCategory; use App\GraphQL\Queries\ExpensesPerCategory; +use App\GraphQL\Queries\TotalPerCategoryTrend; +use App\GraphQL\Queries\TotalExpensesTrend; +use App\GraphQL\Queries\TotalIncomeTrend; +use App\GraphQL\Queries\TotalPerBrandTrend; return [ 'currency' => 'AED', @@ -18,11 +22,10 @@ new TotalExpenses, new IncomePerCategory, new ExpensesPerCategory, + new TotalIncomeTrend, + new TotalExpensesTrend, + new TotalPerCategoryTrend, + new TotalPerBrandTrend, new TotalPerBrand, - - // TotalIncomeTrend - // TotalExpensesTrend - // ExpensesPerCategoryTrend - // TotalPerBrandTrend ] ]; \ No newline at end of file diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 70e04e7..ae77a18 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -45,9 +45,15 @@ type Query { totalExpenses(range: String!): Json totalIncome(range: String!): Json + expensesPerCategory(range: String!): Json incomePerCategory(range: String!): Json totalPerBrand(range: String! category_id: Int): Json + + totalIncomeTrend(range: String!): Json + totalExpensesTrend(range: String!): Json + totalPerCategoryTrend(range: String! id: ID!): Json + totalPerBrandTrend(range: String! id: ID!): Json } type Mutation { diff --git a/public/css/app.css b/public/css/app.css index ada4432..3af9f96 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -796,9 +796,21 @@ select { .h-3 { height: 0.75rem; } +.h-32 { + height: 8rem; +} +.h-28 { + height: 7rem; +} +.h-24 { + height: 6rem; +} .max-h-22 { max-height: 5.625rem; } +.max-h-20 { + max-height: 5rem; +} .min-h-screen { min-height: 100vh; } diff --git a/public/js/app.js b/public/js/app.js index 82d2568..d1d67a4 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -5634,6 +5634,234 @@ function SidePanel(_ref) { /***/ }), +/***/ "./resources/js/Components/TrendMetric.js": +/*!************************************************!*\ + !*** ./resources/js/Components/TrendMetric.js ***! + \************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (/* binding */ TrendMetric) +/* harmony export */ }); +/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js"); +/* harmony import */ var chart_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! chart.js */ "./node_modules/chart.js/dist/chart.esm.js"); +/* harmony import */ var _Card__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./Card */ "./resources/js/Components/Card.js"); +/* harmony import */ var _LoadingView__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./LoadingView */ "./resources/js/Components/LoadingView.js"); +/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! react/jsx-runtime */ "./node_modules/react/jsx-runtime.js"); +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + + + + + + + +chart_js__WEBPACK_IMPORTED_MODULE_1__.Chart.register(chart_js__WEBPACK_IMPORTED_MODULE_1__.LineElement, chart_js__WEBPACK_IMPORTED_MODULE_1__.Tooltip, chart_js__WEBPACK_IMPORTED_MODULE_1__.LineController, chart_js__WEBPACK_IMPORTED_MODULE_1__.CategoryScale, chart_js__WEBPACK_IMPORTED_MODULE_1__.LinearScale, chart_js__WEBPACK_IMPORTED_MODULE_1__.PointElement, chart_js__WEBPACK_IMPORTED_MODULE_1__.Filler); +function TrendMetric(_ref) { + var name = _ref.name, + graphql_query = _ref.graphql_query, + ranges = _ref.ranges, + relation = _ref.relation; + + var _useState = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null), + _useState2 = _slicedToArray(_useState, 2), + data = _useState2[0], + setData = _useState2[1]; + + var _useState3 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(ranges[0].key), + _useState4 = _slicedToArray(_useState3, 2), + selectedRange = _useState4[0], + setSelectedRange = _useState4[1]; + + var _useState5 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null), + _useState6 = _slicedToArray(_useState5, 2), + chartRef = _useState6[0], + setChartRef = _useState6[1]; + + var _useState7 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]), + _useState8 = _slicedToArray(_useState7, 2), + relationData = _useState8[0], + setRelationData = _useState8[1]; + + var _useState9 = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(0), + _useState10 = _slicedToArray(_useState9, 2), + selectedRelationId = _useState10[0], + setSelectedRelationId = _useState10[1]; + + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function () { + if (!relation) { + return; + } + + Api.query(relation.graphql_query + "{ id ".concat(relation.display_using, " }")).then(function (_ref2) { + var data = _ref2.data; + setRelationData(data.data[relation.graphql_query]); + setSelectedRelationId(data.data[relation.graphql_query][0].id); + })["catch"](console.error); + }, []); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function () { + setData(null); + + if (relation) { + if (selectedRelationId) { + Api.query(graphql_query + "(range: \"\"\"".concat(selectedRange, "\"\"\" ").concat(relation.foreign_key, ": ").concat(selectedRelationId, ")")).then(function (_ref3) { + var data = _ref3.data; + return setData(JSON.parse(data.data[graphql_query])); + })["catch"](console.error); + } + + return; + } + + Api.query(graphql_query, selectedRange).then(function (_ref4) { + var data = _ref4.data; + return setData(JSON.parse(data.data[graphql_query])); + })["catch"](console.error); + }, [selectedRelationId, selectedRange]); + (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(function () { + if (data == null) { + return; + } + + if (chartRef != null) { + chartRef.destroy(); + } + + var ctx = document.getElementById(graphql_query).getContext('2d'); + setChartRef(new chart_js__WEBPACK_IMPORTED_MODULE_1__.Chart(ctx, { + type: 'line', + data: { + labels: data.map(function (item) { + return item.label; + }), + datasets: [{ + data: data.map(function (item) { + return item.value; + }), + borderColor: '#0ea5e9', + backgroundColor: 'rgba(14, 165, 233, 0.2)', + pointHoverRadius: 6, + pointRadius: 4, + pointBackgroundColor: '#0ea5e9', + fill: 'start', + tension: 0.4 + }] + }, + options: { + maintainAspectRatio: false, + layout: { + padding: { + left: 0, + right: 0, + bottom: 0, + top: 5 + }, + autoPadding: false + }, + plugins: { + filler: { + propagate: false + }, + tooltip: { + displayColors: false, + backgroundColor: '#fff', + borderColor: '#0ea5e9', + borderWidth: 1, + titleColor: '#0ea5e9', + bodyColor: '#0ea5e9', + xAlign: 'center', + yAlign: 'center' + } + }, + scales: { + y: { + display: false, + beginAtZero: true, + grid: { + display: false + } + }, + x: { + display: false, + grid: { + display: false + } + } + } + } + })); + }, [data]); + + if (data == null) { + return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)(_Card__WEBPACK_IMPORTED_MODULE_2__["default"], { + className: "relative", + children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)(_LoadingView__WEBPACK_IMPORTED_MODULE_3__["default"], {}) + }); + } + + return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxs)(_Card__WEBPACK_IMPORTED_MODULE_2__["default"], { + className: "relative overflow-hidden", + children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("div", { + className: "px-6 py-4", + children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxs)("div", { + className: "flex justify-between items-center mb-2", + children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxs)("div", { + className: "flex items-center", + children: [/*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("h3", { + className: "mr-2 text-base text-gray-700 font-bold", + children: name + }), relation && relationData && /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("select", { + className: "ml-auto min-w-24 h-8 text-xs border-none appearance-none pl-2 pr-6 active:outline-none active:shadow-outline focus:outline-none focus:shadow-outline", + name: "relation", + value: selectedRelationId, + onChange: function onChange(e) { + setSelectedRelationId(e.target.value); + }, + children: relationData.map(function (relationItem) { + return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("option", { + value: relationItem.id, + children: relationItem[relation.display_using] + }, relationItem.id); + }) + })] + }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("select", { + className: "ml-auto min-w-24 h-8 text-xs border-none appearance-none bg-gray-100 pl-2 pr-6 rounded active:outline-none active:shadow-outline focus:outline-none focus:shadow-outline", + name: "range", + value: selectedRange, + onChange: function onChange(e) { + setSelectedRange(e.target.value); + }, + children: ranges.map(function (range) { + return /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("option", { + value: range.key, + children: range.name + }, range.key); + }) + })] + }) + }), /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("div", { + className: "absolute w-full left-0 right-0 bottom-0 h-20", + children: /*#__PURE__*/(0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_4__.jsx)("canvas", { + id: graphql_query + }) + })] + }); +} + +/***/ }), + /***/ "./resources/js/Components/ValidationErrors.js": /*!*****************************************************!*\ !*** ./resources/js/Components/ValidationErrors.js ***! @@ -5806,6 +6034,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js"); /* harmony import */ var _ValueMetric__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ValueMetric */ "./resources/js/Components/ValueMetric.js"); /* harmony import */ var _PartitionMetric__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./PartitionMetric */ "./resources/js/Components/PartitionMetric.js"); +/* harmony import */ var _TrendMetric__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./TrendMetric */ "./resources/js/Components/TrendMetric.js"); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } @@ -5815,9 +6044,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope + var components = { 'value-metric': _ValueMetric__WEBPACK_IMPORTED_MODULE_1__["default"], - 'partition-metric': _PartitionMetric__WEBPACK_IMPORTED_MODULE_2__["default"] + 'partition-metric': _PartitionMetric__WEBPACK_IMPORTED_MODULE_2__["default"], + 'trend-metric': _TrendMetric__WEBPACK_IMPORTED_MODULE_3__["default"] }; var renderComponent = function renderComponent(component, props, children) { if (typeof components[component] !== "undefined") { diff --git a/resources/js/Components/TrendMetric.js b/resources/js/Components/TrendMetric.js new file mode 100644 index 0000000..6629ce4 --- /dev/null +++ b/resources/js/Components/TrendMetric.js @@ -0,0 +1,150 @@ +import { useEffect, useState } from "react"; +import { Chart, LineElement, Tooltip, LineController, CategoryScale, LinearScale, PointElement, Filler } from 'chart.js'; + +import Card from "./Card"; +import LoadingView from "./LoadingView"; + +Chart.register(LineElement, Tooltip, LineController, CategoryScale, LinearScale, PointElement, Filler); + +export default function TrendMetric({ name, graphql_query, ranges, relation }) { + const [data, setData] = useState(null); + const [selectedRange, setSelectedRange] = useState(ranges[0].key); + const [chartRef, setChartRef] = useState(null); + const [relationData, setRelationData] = useState([]); + const [selectedRelationId, setSelectedRelationId] = useState(0); + + useEffect(() => { + if(! relation) { return; } + + Api.query(relation.graphql_query + `{ id ${relation.display_using} }`) + .then(({data}) => { + setRelationData(data.data[relation.graphql_query]) + setSelectedRelationId(data.data[relation.graphql_query][0].id) + }) + .catch(console.error) + }, []) + + useEffect(() => { + setData(null); + + if(relation) { + if (selectedRelationId) { + Api.query(graphql_query + `(range: """${selectedRange}""" ${relation.foreign_key}: ${selectedRelationId})`) + .then(({data}) => setData(JSON.parse(data.data[graphql_query]))) + .catch(console.error) + } + + return; + } + + Api.query(graphql_query, selectedRange) + .then(({data}) => setData(JSON.parse(data.data[graphql_query]))) + .catch(console.error) + }, [selectedRelationId, selectedRange]) + + useEffect(() => { + if(data == null) { return; } + + if(chartRef != null) { + chartRef.destroy() + } + + const ctx = document.getElementById(graphql_query).getContext('2d'); + setChartRef(new Chart(ctx, { + type: 'line', + data: { + labels: data.map(item => item.label), + datasets: [{ + data: data.map(item => item.value), + borderColor: '#0ea5e9', + backgroundColor: 'rgba(14, 165, 233, 0.2)', + pointHoverRadius: 6, + pointRadius: 4, + pointBackgroundColor: '#0ea5e9', + fill: 'start', + tension: 0.4, + }] + }, + options: { + maintainAspectRatio: false, + layout: { + padding: { + left: 0, + right: 0, + bottom: 0, + top: 5 + }, + autoPadding: false + }, + plugins: { + filler: { + propagate: false, + }, + tooltip: { + displayColors: false, + backgroundColor: '#fff', + borderColor: '#0ea5e9', + borderWidth: 1, + titleColor: '#0ea5e9', + bodyColor: '#0ea5e9', + xAlign: 'center', + yAlign: 'center', + } + }, + scales: { + y: { + display: false, + beginAtZero: true, + grid: { + display: false, + }, + }, + x: { + display: false, + grid: { + display: false + }, + } + } + } + })); + }, [data]); + + if(data == null) { + return ( + + + + ) + } + + return ( + +
+
+
+

{ name }

+ + {relation && relationData && } +
+ + +
+
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/resources/js/Components/index.js b/resources/js/Components/index.js index 784dc4e..cc8cf2e 100644 --- a/resources/js/Components/index.js +++ b/resources/js/Components/index.js @@ -1,10 +1,12 @@ import React from "react"; import ValueMetric from "./ValueMetric"; import PartitionMetric from "./PartitionMetric"; +import TrendMetric from "./TrendMetric"; const components = { 'value-metric': ValueMetric, 'partition-metric': PartitionMetric, + 'trend-metric': TrendMetric, }; export const renderComponent = (component, props, children) => { diff --git a/tests/Unit/LastTwelveMonthsTest.php b/tests/Unit/LastTwelveMonthsTest.php new file mode 100644 index 0000000..a5d97a0 --- /dev/null +++ b/tests/Unit/LastTwelveMonthsTest.php @@ -0,0 +1,26 @@ +assertEquals([ + 'key' => 'last-twelve-months', + 'name' => 'Last Twelve Months', + 'start' => '2020-01-01', + 'end' => '2021-01-01', + ], $sut->jsonSerialize()); + } +} diff --git a/tests/Unit/TrendMetricTest.php b/tests/Unit/TrendMetricTest.php new file mode 100644 index 0000000..df9c882 --- /dev/null +++ b/tests/Unit/TrendMetricTest.php @@ -0,0 +1,30 @@ +expectException(\Error::class); + + new TrendMetric; + } + + /** @test */ + public function it_has_correct_component() + { + $sut = new FakeTrendMetric; + + $this->assertEquals('trend-metric', $sut->component()); + } +} + +class FakeTrendMetric extends TrendMetric +{ + +}