diff --git a/README.md b/README.md index 0775706..96f57df 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ See a [live demo](https://tremor-redwood-dashboard-demo.netlify.app) dashboard! There is a static data version of the dashboard as well as a dynamic version that pulls from a SQLite database for KPI, SalesPeople and CompanyPerformance. -Scaffolding has been added to edit these datapoints so can be reflected in the dynamic dashboard. +Scaffolding has been added to edit these data points so can be reflected in the dynamic dashboard. ### Static Screens diff --git a/api/db/migrations/20230501124209_demo/migration.sql b/api/db/migrations/20230904175506_/migration.sql similarity index 84% rename from api/db/migrations/20230501124209_demo/migration.sql rename to api/db/migrations/20230904175506_/migration.sql index 8d25824..29a5ce1 100644 --- a/api/db/migrations/20230501124209_demo/migration.sql +++ b/api/db/migrations/20230904175506_/migration.sql @@ -14,10 +14,10 @@ CREATE TABLE "Kpi" ( -- CreateTable CREATE TABLE "CompanyPerformance" ( "id" SERIAL NOT NULL, - "date" TEXT NOT NULL, - "sales" DECIMAL(65,30) NOT NULL, - "profit" DECIMAL(65,30) NOT NULL, - "customers" INTEGER NOT NULL, + "date" TIMESTAMP(3) NOT NULL, + "Sales" DECIMAL(65,30) NOT NULL, + "Profit" DECIMAL(65,30) NOT NULL, + "Customers" INTEGER NOT NULL, CONSTRAINT "CompanyPerformance_pkey" PRIMARY KEY ("id") ); diff --git a/api/db/schema.prisma b/api/db/schema.prisma index d6b0f5b..b54c2cc 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -1,5 +1,5 @@ datasource db { - provider = "sqlite" + provider = "postgresql" url = env("DATABASE_URL") } @@ -19,11 +19,11 @@ model Kpi { } model CompanyPerformance { - id Int @id @default(autoincrement()) - date String - sales Decimal - profit Decimal - customers Int + id Int @id @default(autoincrement()) + date DateTime + Sales Decimal + Profit Decimal + Customers Int } model SalesPerson { diff --git a/api/package.json b/api/package.json index 0968bd5..dd034e1 100644 --- a/api/package.json +++ b/api/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@netlify/functions": "1.6.0", - "@redwoodjs/api": "5.2.3", - "@redwoodjs/graphql-server": "5.2.3" + "@redwoodjs/api": "6.1.0", + "@redwoodjs/graphql-server": "6.1.0" } } diff --git a/api/src/graphql/companyPerformances.sdl.ts b/api/src/graphql/companyPerformances.sdl.ts index 0e508b7..6d208fc 100644 --- a/api/src/graphql/companyPerformances.sdl.ts +++ b/api/src/graphql/companyPerformances.sdl.ts @@ -1,10 +1,10 @@ export const schema = gql` type CompanyPerformance { id: Int! - date: String! - sales: Float! - profit: Float! - customers: Int! + date: DateTime! + Sales: Float! + Profit: Float! + Customers: Int! } type Query { @@ -13,17 +13,17 @@ export const schema = gql` } input CreateCompanyPerformanceInput { - date: String! - sales: Float! - profit: Float! - customers: Int! + date: DateTime! + Sales: Float! + Profit: Float! + Customers: Int! } input UpdateCompanyPerformanceInput { - date: String - sales: Float - profit: Float - customers: Int + date: DateTime + Sales: Float + Profit: Float + Customers: Int } type Mutation { diff --git a/api/src/services/companyPerformances/companyPerformances.scenarios.ts b/api/src/services/companyPerformances/companyPerformances.scenarios.ts index 178af79..ebd7f71 100644 --- a/api/src/services/companyPerformances/companyPerformances.scenarios.ts +++ b/api/src/services/companyPerformances/companyPerformances.scenarios.ts @@ -5,18 +5,18 @@ export const standard = defineScenario({ companyPerformance: { one: { data: { - date: 'String', - sales: 4123311.0664987317, - profit: 9629717.225889865, - customers: 6552864, + date: '2023-09-04T16:42:31.893Z', + Sales: 4479699.532100878, + Profit: 2344580.2038503485, + Customers: 9995248, }, }, two: { data: { - date: 'String', - sales: 3407878.1655372214, - profit: 6043919.859423452, - customers: 4763215, + date: '2023-09-04T16:42:31.893Z', + Sales: 7447966.365788998, + Profit: 8766114.289045736, + Customers: 7396524, }, }, }, diff --git a/api/src/services/companyPerformances/companyPerformances.test.ts b/api/src/services/companyPerformances/companyPerformances.test.ts index e80a9e3..c12ba0c 100644 --- a/api/src/services/companyPerformances/companyPerformances.test.ts +++ b/api/src/services/companyPerformances/companyPerformances.test.ts @@ -41,17 +41,17 @@ describe('companyPerformances', () => { scenario('creates a companyPerformance', async () => { const result = await createCompanyPerformance({ input: { - date: 'String', - sales: 2494455.5866956785, - profit: 9237431.41915011, - customers: 1490105, + date: '2023-09-04T16:42:31.883Z', + Sales: 1710517.558160818, + Profit: 378941.0764930223, + Customers: 3647439, }, }) - expect(result.date).toEqual('String') - expect(result.sales).toEqual(new Prisma.Decimal(2494455.5866956785)) - expect(result.profit).toEqual(new Prisma.Decimal(9237431.41915011)) - expect(result.customers).toEqual(1490105) + expect(result.date).toEqual(new Date('2023-09-04T16:42:31.883Z')) + expect(result.Sales).toEqual(new Prisma.Decimal(1710517.558160818)) + expect(result.Profit).toEqual(new Prisma.Decimal(378941.0764930223)) + expect(result.Customers).toEqual(3647439) }) scenario( @@ -62,10 +62,10 @@ describe('companyPerformances', () => { })) as CompanyPerformance const result = await updateCompanyPerformance({ id: original.id, - input: { date: 'String2' }, + input: { date: '2023-09-05T16:42:31.883Z' }, }) - expect(result.date).toEqual('String2') + expect(result.date).toEqual(new Date('2023-09-05T16:42:31.883Z')) } ) diff --git a/api/src/services/companyPerformances/companyPerformances.ts b/api/src/services/companyPerformances/companyPerformances.ts index c3ca122..deffea0 100644 --- a/api/src/services/companyPerformances/companyPerformances.ts +++ b/api/src/services/companyPerformances/companyPerformances.ts @@ -4,7 +4,7 @@ import { db } from 'src/lib/db' export const companyPerformances: QueryResolvers['companyPerformances'] = () => { - return db.companyPerformance.findMany() + return db.companyPerformance.findMany({ orderBy: { date: 'asc' } }) } export const companyPerformance: QueryResolvers['companyPerformance'] = ({ diff --git a/api/src/services/salesPeople/salesPeople.ts b/api/src/services/salesPeople/salesPeople.ts index 3490829..e4e1f16 100644 --- a/api/src/services/salesPeople/salesPeople.ts +++ b/api/src/services/salesPeople/salesPeople.ts @@ -3,7 +3,7 @@ import type { QueryResolvers, MutationResolvers } from 'types/graphql' import { db } from 'src/lib/db' export const salesPeople: QueryResolvers['salesPeople'] = () => { - return db.salesPerson.findMany({orderBy: { name: 'asc' }}) + return db.salesPerson.findMany({ orderBy: { name: 'asc' } }) } export const salesPerson: QueryResolvers['salesPerson'] = ({ id }) => { diff --git a/package.json b/package.json index 427715d..5328c58 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ ] }, "devDependencies": { - "@redwoodjs/core": "5.2.3", + "@redwoodjs/core": "6.1.0", "prettier-plugin-tailwindcss": "^0.3.0" }, "eslintConfig": { diff --git a/web/config/tailwind.config.js b/web/config/tailwind.config.js index 0818510..1b5c2e8 100644 --- a/web/config/tailwind.config.js +++ b/web/config/tailwind.config.js @@ -6,7 +6,127 @@ module.exports = { '../node_modules/@tremor/**/*.{js,ts,jsx,tsx}', ], theme: { - extend: {}, + transparent: 'transparent', + current: 'currentColor', + + extend: { + colors: { + // light mode + tremor: { + brand: { + faint: '#eff6ff', // blue-50 + muted: '#bfdbfe', // blue-200 + subtle: '#60a5fa', // blue-400 + DEFAULT: '#3b82f6', // blue-500 + emphasis: '#1d4ed8', // blue-700 + inverted: '#ffffff', // white + }, + background: { + muted: '#f9fafb', // gray-50 + subtle: '#f3f4f6', // gray-100 + DEFAULT: '#ffffff', // white + emphasis: '#374151', // gray-700 + }, + border: { + DEFAULT: '#e5e7eb', // gray-200 + }, + ring: { + DEFAULT: '#e5e7eb', // gray-200 + }, + content: { + subtle: '#9ca3af', // gray-400 + DEFAULT: '#6b7280', // gray-500 + emphasis: '#374151', // gray-700 + strong: '#111827', // gray-900 + inverted: '#ffffff', // white + }, + }, + // dark mode + 'dark-tremor': { + brand: { + faint: '#0B1229', // custom + muted: '#172554', // blue-950 + subtle: '#1e40af', // blue-800 + DEFAULT: '#3b82f6', // blue-500 + emphasis: '#60a5fa', // blue-400 + inverted: '#030712', // gray-950 + }, + background: { + muted: '#131A2B', // custom + subtle: '#1f2937', // gray-800 + DEFAULT: '#111827', // gray-900 + emphasis: '#d1d5db', // gray-300 + }, + border: { + DEFAULT: '#1f2937', // gray-800 + }, + ring: { + DEFAULT: '#1f2937', // gray-800 + }, + content: { + subtle: '#4b5563', // gray-600 + DEFAULT: '#6b7280', // gray-600 + emphasis: '#e5e7eb', // gray-200 + strong: '#f9fafb', // gray-50 + inverted: '#000000', // black + }, + }, + }, + boxShadow: { + // light + 'tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)', + 'tremor-card': + '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + 'tremor-dropdown': + '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + // dark + 'dark-tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)', + 'dark-tremor-card': + '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + 'dark-tremor-dropdown': + '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', + }, + borderRadius: { + 'tremor-small': '0.375rem', + 'tremor-default': '0.5rem', + 'tremor-full': '9999px', + }, + fontSize: { + 'tremor-label': ['0.75rem'], + 'tremor-default': ['0.875rem', { lineHeight: '1.25rem' }], + 'tremor-title': ['1.125rem', { lineHeight: '1.75rem' }], + 'tremor-metric': ['1.875rem', { lineHeight: '2.25rem' }], + }, + }, }, - plugins: [require('@tailwindcss/forms')], + safelist: [ + { + pattern: + /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, + variants: ['hover', 'ui-selected'], + }, + { + pattern: + /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, + variants: ['hover', 'ui-selected'], + }, + { + pattern: + /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, + variants: ['hover', 'ui-selected'], + }, + { + pattern: + /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, + }, + { + pattern: + /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, + }, + { + pattern: + /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, + }, + ], + plugins: [require('@tailwindcss/forms'), require('@headlessui/tailwindcss')], } diff --git a/web/package.json b/web/package.json index 6e992d7..4361e33 100644 --- a/web/package.json +++ b/web/package.json @@ -14,18 +14,19 @@ }, "dependencies": { "@heroicons/react": "1.0.6", - "@redwoodjs/forms": "5.2.3", - "@redwoodjs/router": "5.2.3", - "@redwoodjs/web": "5.2.3", + "@redwoodjs/forms": "6.1.0", + "@redwoodjs/router": "6.1.0", + "@redwoodjs/web": "6.1.0", "@tailwindcss/forms": "0.5.3", - "@tremor/react": "2.9.1", - "date-fns": "^2.30.0", + "@tremor/react": "3.7.0", + "date-fns": "2.30.0", "humanize-string": "2.1.0", "prop-types": "15.8.1", "react": "18.2.0", "react-dom": "18.2.0" }, "devDependencies": { + "@redwoodjs/vite": "6.1.0", "autoprefixer": "10.4.14", "postcss": "8.4.23", "postcss-loader": "7.3.1", diff --git a/web/src/components/ChartView/ChartView.tsx b/web/src/components/ChartView/ChartView.tsx index 469df6d..8f10052 100644 --- a/web/src/components/ChartView/ChartView.tsx +++ b/web/src/components/ChartView/ChartView.tsx @@ -1,87 +1,111 @@ import { useState } from 'react' -import { InformationCircleIcon } from '@heroicons/react/outline' +import { InformationCircleIcon } from '@heroicons/react/solid' import { - AreaChart, - Card, Flex, + Title, Icon, + TabGroup, + TabList, + Tab, + AreaChart, Text, - Title, - Toggle, - ToggleItem, + Color, } from '@tremor/react' +import { format, parseISO } from 'date-fns' -export type PerformanceData = { - date: string - sales: number - profit: number - customers: number -} +const usNumberformatter = (number: number, decimals = 0) => + Intl.NumberFormat('us', { + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }) + .format(Number(number)) + .toString() -interface Props { - performance: PerformanceData[] +const formatters: { [key: string]: any } = { + Sales: (number: number) => `$ ${usNumberformatter(number)}`, + Profit: (number: number) => `$ ${usNumberformatter(number)}`, + Customers: (number: number) => `${usNumberformatter(number)}`, + Delta: (number: number) => `${usNumberformatter(number, 2)}%`, } -// Basic formatters for the chart values -const dollarFormatter = (value: number) => - `$ ${Intl.NumberFormat('us').format(value).toString()}` +import type { DailyPerformance } from 'src/data/performance' + +export default function ChartView({ + performance, + kpiList, +}: { + performance: DailyPerformance[] + kpiList: string[] +}) { + const [selectedIndex, setSelectedIndex] = useState(0) + const selectedKpi = kpiList[selectedIndex] -const numberFormatter = (value: number) => - `${Intl.NumberFormat('us').format(value).toString()}` + const formatPerformance = (performance: DailyPerformance[]) => { + return performance.map((item) => { + try { + return { ...item, date: format(item.date, 'MMM dd') } + } catch { + return { + ...item, + date: format(parseISO(item.date as unknown as string), 'MMM dd'), + } + } + }) + } -const ChartView = ({ performance }: Props) => { - const [selectedKpi, setSelectedKpi] = useState('sales') - // d = performance - // map formatters by selectedKpi - const formatters: { [key: string]: any } = { - Sales: dollarFormatter, - Profit: dollarFormatter, - Customers: numberFormatter, + const areaChartArgs = { + className: 'mt-5 h-72', + data: formatPerformance(performance), + index: 'date', + categories: [selectedKpi], + colors: ['blue'] as Color[], + showLegend: false, + valueFormatter: formatters[selectedKpi], + yAxisWidth: 56, } return ( - + <>
Performance History - Daily increase or decrease per domain + Daily change per domain
-
- setSelectedKpi(value)} - > - - - - +
+ + + Sales + Profit + Customers + +
- - + {/* web */} +
+ +
+ {/* mobile */} +
+ +
+ ) } - -export default ChartView diff --git a/web/src/components/ChartViewsCell/ChartViewsCell.tsx b/web/src/components/ChartViewsCell/ChartViewsCell.tsx index 90d357c..f0155bf 100644 --- a/web/src/components/ChartViewsCell/ChartViewsCell.tsx +++ b/web/src/components/ChartViewsCell/ChartViewsCell.tsx @@ -3,15 +3,17 @@ import type { ChartViewsQuery } from 'types/graphql' import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' import ChartView from 'src/components/ChartView/ChartView' +import { kpiList } from 'src/data/kpis' +import { DailyPerformance } from 'src/data/performance' export const QUERY = gql` query ChartViewsQuery { companyPerformances { id date - sales - profit - customers + Sales + Profit + Customers } } ` @@ -27,5 +29,10 @@ export const Failure = ({ error }: CellFailureProps) => ( export const Success = ({ companyPerformances, }: CellSuccessProps) => { - return + return ( + + ) } diff --git a/web/src/components/CompanyPerformance/CompanyPerformance/CompanyPerformance.tsx b/web/src/components/CompanyPerformance/CompanyPerformance/CompanyPerformance.tsx index 1de62cb..01523eb 100644 --- a/web/src/components/CompanyPerformance/CompanyPerformance/CompanyPerformance.tsx +++ b/web/src/components/CompanyPerformance/CompanyPerformance/CompanyPerformance.tsx @@ -2,7 +2,7 @@ import { Link, routes, navigate } from '@redwoodjs/router' import { useMutation } from '@redwoodjs/web' import { toast } from '@redwoodjs/web/toast' -import {} from 'src/lib/formatters' +import { timeTag } from 'src/lib/formatters' import type { DeleteCompanyPerformanceMutationVariables, @@ -63,19 +63,19 @@ const CompanyPerformance = ({ companyPerformance }: Props) => { Date - {companyPerformance.date} + {timeTag(companyPerformance.date)} Sales - {companyPerformance.sales} + {companyPerformance.Sales} Profit - {companyPerformance.profit} + {companyPerformance.Profit} Customers - {companyPerformance.customers} + {companyPerformance.Customers} diff --git a/web/src/components/CompanyPerformance/CompanyPerformanceCell/CompanyPerformanceCell.tsx b/web/src/components/CompanyPerformance/CompanyPerformanceCell/CompanyPerformanceCell.tsx index 1672b82..48439fd 100644 --- a/web/src/components/CompanyPerformance/CompanyPerformanceCell/CompanyPerformanceCell.tsx +++ b/web/src/components/CompanyPerformance/CompanyPerformanceCell/CompanyPerformanceCell.tsx @@ -9,9 +9,9 @@ export const QUERY = gql` companyPerformance: companyPerformance(id: $id) { id date - sales - profit - customers + Sales + Profit + Customers } } ` diff --git a/web/src/components/CompanyPerformance/CompanyPerformanceForm/CompanyPerformanceForm.tsx b/web/src/components/CompanyPerformance/CompanyPerformanceForm/CompanyPerformanceForm.tsx index 308a92e..324fa38 100644 --- a/web/src/components/CompanyPerformance/CompanyPerformanceForm/CompanyPerformanceForm.tsx +++ b/web/src/components/CompanyPerformance/CompanyPerformanceForm/CompanyPerformanceForm.tsx @@ -1,19 +1,26 @@ +import type { + EditCompanyPerformanceById, + UpdateCompanyPerformanceInput, +} from 'types/graphql' + import { Form, FormError, FieldError, Label, + DatetimeLocalField, TextField, NumberField, Submit, } from '@redwoodjs/forms' - -import type { - EditCompanyPerformanceById, - UpdateCompanyPerformanceInput, -} from 'types/graphql' import type { RWGqlError } from '@redwoodjs/forms' +const formatDatetime = (value) => { + if (value) { + return value.replace(/:\d{2}\.\d{3}\w/, '') + } +} + type FormCompanyPerformance = NonNullable< EditCompanyPerformanceById['companyPerformance'] > @@ -51,9 +58,9 @@ const CompanyPerformanceForm = (props: CompanyPerformanceFormProps) => { Date - { - + - + - +
diff --git a/web/src/components/CompanyPerformance/CompanyPerformances/CompanyPerformances.tsx b/web/src/components/CompanyPerformance/CompanyPerformances/CompanyPerformances.tsx index 1c6e8a0..45af537 100644 --- a/web/src/components/CompanyPerformance/CompanyPerformances/CompanyPerformances.tsx +++ b/web/src/components/CompanyPerformance/CompanyPerformances/CompanyPerformances.tsx @@ -3,7 +3,7 @@ import { useMutation } from '@redwoodjs/web' import { toast } from '@redwoodjs/web/toast' import { QUERY } from 'src/components/CompanyPerformance/CompanyPerformancesCell' -import { truncate } from 'src/lib/formatters' +import { timeTag, truncate } from 'src/lib/formatters' import type { DeleteCompanyPerformanceMutationVariables, @@ -65,10 +65,10 @@ const CompanyPerformancesList = ({ {companyPerformances.map((companyPerformance) => ( {truncate(companyPerformance.id)} - {truncate(companyPerformance.date)} - {truncate(companyPerformance.sales)} - {truncate(companyPerformance.profit)} - {truncate(companyPerformance.customers)} + {timeTag(companyPerformance.date)} + {truncate(companyPerformance.Sales)} + {truncate(companyPerformance.Profit)} + {truncate(companyPerformance.Customers)}