diff --git a/front/src/interfaces/company.interface.test.ts b/front/src/interfaces/company.interface.test.ts new file mode 100644 index 000000000..765f357ad --- /dev/null +++ b/front/src/interfaces/company.interface.test.ts @@ -0,0 +1,68 @@ +import { mapGqlCompany, mapCompany } from './company.interface'; + +describe('mapCompany', () => { + it('should map company', () => { + const now = new Date(); + now.setMilliseconds(0); + const company = mapCompany({ + id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', + name: 'ACME', + domain_name: 'exmaple.com', + created_at: now.toUTCString(), + account_owner: { + id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87', + email: 'john@example.com', + displayName: 'John Doe', + }, + employees: 10, + address: '1 Infinite Loop, 95014 Cupertino, California, USA', + }); + expect(company.id).toBe('7dfbc3f7-6e5e-4128-957e-8d86808cdf6b'); + expect(company.name).toBe('ACME'); + expect(company.domain_name).toBe('exmaple.com'); + expect(company.creationDate).toEqual(now); + expect(company.accountOwner.id).toBe( + '7af20dea-0412-4c4c-8b13-d6f0e6e09e87', + ); + expect(company.accountOwner.email).toBe('john@example.com'); + expect(company.accountOwner.first_name).toBe('John'); + expect(company.accountOwner.last_name).toBe('Doe'); + expect(company.employees).toBe(10); + expect(company.address).toBe( + '1 Infinite Loop, 95014 Cupertino, California, USA', + ); + }); + + it('should map company back', () => { + const now = new Date(); + now.setMilliseconds(0); + const company = mapGqlCompany({ + id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', + name: 'ACME', + domain_name: 'exmaple.com', + employees: 10, + address: '1 Infinite Loop, 95014 Cupertino, California, USA', + opportunities: [], + accountOwner: { + id: '522d4ec4-c46b-4360-a0a7-df8df170be81', + email: 'john@example.com', + first_name: 'John', + last_name: 'Doe', + }, + creationDate: now, + }); + expect(company.id).toBe('7dfbc3f7-6e5e-4128-957e-8d86808cdf6b'); + expect(company.name).toBe('ACME'); + expect(company.domain_name).toBe('exmaple.com'); + expect(company.created_at).toEqual(now.toUTCString()); + expect(company.account_owner.id).toBe( + '522d4ec4-c46b-4360-a0a7-df8df170be81', + ); + expect(company.account_owner.email).toBe('john@example.com'); + expect(company.account_owner.displayName).toBe('John Doe'); + expect(company.employees).toBe(10); + expect(company.address).toBe( + '1 Infinite Loop, 95014 Cupertino, California, USA', + ); + }); +}); diff --git a/front/src/interfaces/company.interface.ts b/front/src/interfaces/company.interface.ts index a9fb83803..fd3cee50c 100644 --- a/front/src/interfaces/company.interface.ts +++ b/front/src/interfaces/company.interface.ts @@ -1,5 +1,59 @@ +import { User } from './user.interface'; + +export interface Opportunity { + name: string; + icon: string; +} + export interface Company { id: string; name: string; domain_name: string; + employees: number; + address: string; + opportunities: Opportunity[]; + accountOwner: User; + creationDate: Date; } + +export type GraphqlQueryAccountOwner = { + id: string; + email: string; + displayName: string; +}; + +export type GraphqlQueryCompany = { + id: string; + name: string; + domain_name: string; + account_owner: GraphqlQueryAccountOwner; + employees: number; + address: string; + created_at: string; +}; + +export const mapCompany = (company: GraphqlQueryCompany): Company => ({ + ...company, + name: company.name, + domain_name: company.domain_name, + accountOwner: { + id: company.account_owner.id, + email: company.account_owner.email, + first_name: company.account_owner.displayName.split(' ').shift() || '', + last_name: company.account_owner.displayName.split(' ').slice(1).join(' '), + }, + creationDate: new Date(company.created_at), + opportunities: [{ name: 'Sales Pipeline', icon: '' }], +}); + +export const mapGqlCompany = (company: Company): GraphqlQueryCompany => ({ + ...company, + name: company.name, + domain_name: company.domain_name, + created_at: company.creationDate.toUTCString(), + account_owner: { + id: company.accountOwner.id, + email: company.accountOwner.email, + displayName: `${company.accountOwner.first_name} ${company.accountOwner.last_name}`, + }, +}); diff --git a/front/src/interfaces/person.interface.ts b/front/src/interfaces/person.interface.ts index f688b089c..1be9f6236 100644 --- a/front/src/interfaces/person.interface.ts +++ b/front/src/interfaces/person.interface.ts @@ -6,7 +6,10 @@ export type Person = { fullName: string; picture?: string; email: string; - company: Company; + company: Omit< + Company, + 'employees' | 'address' | 'opportunities' | 'accountOwner' | 'creationDate' + >; phone: string; creationDate: Date; pipe: Pipe; diff --git a/front/src/pages/companies/Companies.tsx b/front/src/pages/companies/Companies.tsx index b3b1fea1b..40b1032ef 100644 --- a/front/src/pages/companies/Companies.tsx +++ b/front/src/pages/companies/Companies.tsx @@ -1,10 +1,45 @@ -import { faBuildings } from '@fortawesome/pro-regular-svg-icons'; +import { faBuildings, faList } from '@fortawesome/pro-regular-svg-icons'; import WithTopBarContainer from '../../layout/containers/WithTopBarContainer'; +import styled from '@emotion/styled'; +import { useState, useCallback } from 'react'; +import { + CompaniesSelectedSortType, + defaultOrderBy, + reduceSortsToOrderBy, + useCompaniesQuery, +} from '../../services/companies'; +import Table from '../../components/table/Table'; +import { mapCompany } from '../../interfaces/company.interface'; +import { companiesColumns, sortsAvailable } from './companies-table'; + +const StyledCompaniesContainer = styled.div` + display: flex; + width: 100%; +`; function Companies() { + const [, setSorts] = useState([] as Array); + const [orderBy, setOrderBy] = useState(defaultOrderBy); + + const updateSorts = useCallback((sorts: Array) => { + setSorts(sorts); + setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy); + }, []); + + const { data } = useCompaniesQuery(orderBy); + return ( - <> + + + ); } diff --git a/front/src/pages/companies/__stories__/Companies.stories.tsx b/front/src/pages/companies/__stories__/Companies.stories.tsx index 447e1ac20..cd28a0ce2 100644 --- a/front/src/pages/companies/__stories__/Companies.stories.tsx +++ b/front/src/pages/companies/__stories__/Companies.stories.tsx @@ -2,6 +2,9 @@ import { MemoryRouter } from 'react-router-dom'; import Companies from '../Companies'; import { ThemeProvider } from '@emotion/react'; import { lightTheme } from '../../../layout/styles/themes'; +import { GET_COMPANIES } from '../../../services/companies'; +import { defaultData } from './mock-data'; +import { MockedProvider } from '@apollo/client/testing'; const component = { title: 'Companies', @@ -10,10 +13,28 @@ const component = { export default component; +const mocks = [ + { + request: { + query: GET_COMPANIES, + variables: { + orderBy: [{ name: 'asc' }], + }, + }, + result: { + data: { + companies: defaultData, + }, + }, + }, +]; + export const CompaniesDefault = () => ( - - - - - + + + + + + + ); diff --git a/front/src/pages/companies/__stories__/mock-data.ts b/front/src/pages/companies/__stories__/mock-data.ts new file mode 100644 index 000000000..a0aa9a36b --- /dev/null +++ b/front/src/pages/companies/__stories__/mock-data.ts @@ -0,0 +1,17 @@ +import { GraphqlQueryCompany } from '../../../interfaces/company.interface'; + +export const defaultData: Array = [ + { + id: 'f121ab32-fac4-4b8c-9a3d-150c877319c2', + name: 'ACME', + domain_name: 'example.com', + account_owner: { + id: '91510aa5-ede6-451f-8029-a7fa69e4bad6', + email: 'john@example.com', + displayName: 'John Doe', + }, + employees: 10, + address: '1 Infinity Loop, 95014 Cupertino, California', + created_at: new Date().toUTCString(), + }, +]; diff --git a/front/src/pages/companies/__tests__/Companies.test.tsx b/front/src/pages/companies/__tests__/Companies.test.tsx index dd1007aa8..c6f735c24 100644 --- a/front/src/pages/companies/__tests__/Companies.test.tsx +++ b/front/src/pages/companies/__tests__/Companies.test.tsx @@ -1,10 +1,15 @@ -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { CompaniesDefault } from '../__stories__/Companies.stories'; -it('Checks the Companies page render', () => { +it('Checks the Companies page render', async () => { const { getByTestId } = render(); const title = getByTestId('top-bar-title'); expect(title).toHaveTextContent('Companies'); + + await waitFor(() => { + const row = getByTestId('row-id-0'); + expect(row).toBeDefined(); + }); }); diff --git a/front/src/pages/companies/companies-table.tsx b/front/src/pages/companies/companies-table.tsx new file mode 100644 index 000000000..69a685721 --- /dev/null +++ b/front/src/pages/companies/companies-table.tsx @@ -0,0 +1,116 @@ +import { createColumnHelper } from '@tanstack/react-table'; +import { Company } from '../../interfaces/company.interface'; +import { OrderByFields } from '../../services/companies'; +import ColumnHead from '../../components/table/ColumnHead'; +import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer'; +import Checkbox from '../../components/form/Checkbox'; +import CompanyChip from '../../components/chips/CompanyChip'; +import EditableCell from '../../components/table/EditableCell'; +import PipeChip from '../../components/chips/PipeChip'; +import { faCalendar } from '@fortawesome/pro-regular-svg-icons'; +import ClickableCell from '../../components/table/ClickableCell'; +import PersonChip from '../../components/chips/PersonChip'; +import { SortType } from '../../components/table/table-header/interface'; + +export const sortsAvailable = [ + { + key: 'name', + label: 'Name', + icon: undefined, + }, + { + key: 'domain_name', + label: 'Domain', + icon: undefined, + }, +] satisfies Array>; + +const columnHelper = createColumnHelper(); +export const companiesColumns = [ + columnHelper.accessor('name', { + header: () => , + cell: (props) => ( + + + + + ), + }), + columnHelper.accessor('employees', { + header: () => , + cell: (props) => ( + { + const company = props.row.original; + company.employees = parseInt(value); + // TODO: update company + }} + /> + ), + }), + columnHelper.accessor('domain_name', { + header: () => , + cell: (props) => ( + { + const company = props.row.original; + company.domain_name = value; + // TODO: update company + }} + /> + ), + }), + columnHelper.accessor('address', { + header: () => , + cell: (props) => ( + { + const company = props.row.original; + company.address = value; + // TODO: update company + }} + /> + ), + }), + columnHelper.accessor('opportunities', { + header: () => , + cell: (props) => ( + + {props.row.original.opportunities.map((opportunity) => ( + + ))} + + ), + }), + columnHelper.accessor('creationDate', { + header: () => , + cell: (props) => ( + + {new Intl.DateTimeFormat(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }).format(props.row.original.creationDate)} + + ), + }), + columnHelper.accessor('accountOwner', { + header: () => , + cell: (props) => ( + + + + ), + }), +]; diff --git a/front/src/services/companies/index.ts b/front/src/services/companies/index.ts new file mode 100644 index 000000000..c7396734d --- /dev/null +++ b/front/src/services/companies/index.ts @@ -0,0 +1 @@ +export * from './select'; diff --git a/front/src/services/companies/select.test.ts b/front/src/services/companies/select.test.ts new file mode 100644 index 000000000..73eb73630 --- /dev/null +++ b/front/src/services/companies/select.test.ts @@ -0,0 +1,12 @@ +import { CompaniesSelectedSortType, reduceSortsToOrderBy } from './select'; + +describe('reduceSortsToOrderBy', () => { + it('should return an array of objects with the id as key and the order as value', () => { + const sorts = [ + { key: 'name', label: 'name', order: 'asc' }, + { key: 'domain_name', label: 'domain_name', order: 'desc' }, + ] satisfies CompaniesSelectedSortType[]; + const result = reduceSortsToOrderBy(sorts); + expect(result).toEqual([{ name: 'asc', domain_name: 'desc' }]); + }); +}); diff --git a/front/src/services/companies/select.ts b/front/src/services/companies/select.ts new file mode 100644 index 000000000..481186268 --- /dev/null +++ b/front/src/services/companies/select.ts @@ -0,0 +1,56 @@ +import { QueryResult, gql, useQuery } from '@apollo/client'; +import { Order_By, Companies_Order_By } from '../../generated/graphql'; +import { GraphqlQueryCompany } from '../../interfaces/company.interface'; +import { SelectedSortType } from '../../components/table/table-header/interface'; + +export type OrderByFields = keyof Companies_Order_By | 'domain_name' | 'name'; + +export type CompaniesSelectedSortType = SelectedSortType; + +const mapOrder = (order: 'asc' | 'desc'): Order_By => { + return order === 'asc' ? Order_By.Asc : Order_By.Desc; +}; + +export const reduceSortsToOrderBy = ( + sorts: Array, +): Companies_Order_By[] => { + const mappedSorts = sorts.reduce((acc, sort) => { + const id = sort.key; + const order = mapOrder(sort.order); + acc[id] = order; + return acc; + }, {} as Companies_Order_By); + return [mappedSorts]; +}; + +export const GET_COMPANIES = gql` + query GetCompanies($orderBy: [companies_order_by!]) { + companies(order_by: $orderBy) { + id + domain_name + name + created_at + address + employees + account_owner { + id + email + displayName + } + } + } +`; + +export function useCompaniesQuery( + orderBy: Companies_Order_By[], +): QueryResult<{ companies: GraphqlQueryCompany[] }> { + return useQuery<{ companies: GraphqlQueryCompany[] }>(GET_COMPANIES, { + variables: { orderBy }, + }); +} + +export const defaultOrderBy: Companies_Order_By[] = [ + { + name: Order_By.Asc, + }, +]; diff --git a/hasura/metadata/databases/default/tables/auth_users.yaml b/hasura/metadata/databases/default/tables/auth_users.yaml index dcc6723fe..62d24c466 100644 --- a/hasura/metadata/databases/default/tables/auth_users.yaml +++ b/hasura/metadata/databases/default/tables/auth_users.yaml @@ -129,6 +129,14 @@ array_relationships: table: name: user_providers schema: auth +select_permissions: + - role: user + permission: + columns: + - display_name + - email + - id + filter: {} event_triggers: - name: user-created definition: @@ -142,4 +150,4 @@ event_triggers: webhook: '{{HASURA_EVENT_HANDLER_URL}}' headers: - name: secret-header - value: secret + value: secret \ No newline at end of file diff --git a/hasura/metadata/databases/default/tables/public_companies.yaml b/hasura/metadata/databases/default/tables/public_companies.yaml index 1a7faab88..e38c9640b 100644 --- a/hasura/metadata/databases/default/tables/public_companies.yaml +++ b/hasura/metadata/databases/default/tables/public_companies.yaml @@ -2,6 +2,15 @@ table: name: companies schema: public object_relationships: + - name: account_owner + using: + manual_configuration: + column_mapping: + account_owner_id: id + insertion_order: null + remote_table: + name: users + schema: auth - name: workspace using: foreign_key_constraint_on: workspace_id @@ -14,6 +23,9 @@ insert_permissions: columns: - id - workspace_id + - account_owner_id + - address + - employees - name - domain_name - created_at @@ -25,6 +37,9 @@ select_permissions: columns: - domain_name - name + - account_owner_id + - address + - employees - created_at - deleted_at - updated_at @@ -39,6 +54,9 @@ update_permissions: columns: - domain_name - name + - employees + - address + - account_owner_id - created_at - deleted_at - updated_at @@ -53,4 +71,4 @@ delete_permissions: permission: filter: workspace_id: - _eq: x-hasura-workspace-id + _eq: x-hasura-workspace-id \ No newline at end of file diff --git a/hasura/migrations/default/1682514543325_alter_table_public_companies_add_column_account_owner_id/down.sql b/hasura/migrations/default/1682514543325_alter_table_public_companies_add_column_account_owner_id/down.sql new file mode 100644 index 000000000..4dedc88c1 --- /dev/null +++ b/hasura/migrations/default/1682514543325_alter_table_public_companies_add_column_account_owner_id/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."companies" add column "account_owner_id" uuid +-- null; diff --git a/hasura/migrations/default/1682514543325_alter_table_public_companies_add_column_account_owner_id/up.sql b/hasura/migrations/default/1682514543325_alter_table_public_companies_add_column_account_owner_id/up.sql new file mode 100644 index 000000000..9679696d2 --- /dev/null +++ b/hasura/migrations/default/1682514543325_alter_table_public_companies_add_column_account_owner_id/up.sql @@ -0,0 +1,2 @@ +alter table "public"."companies" add column "account_owner_id" uuid + null; diff --git a/hasura/migrations/default/1682514575594_alter_table_public_companies_add_column_employees/down.sql b/hasura/migrations/default/1682514575594_alter_table_public_companies_add_column_employees/down.sql new file mode 100644 index 000000000..e17b363f3 --- /dev/null +++ b/hasura/migrations/default/1682514575594_alter_table_public_companies_add_column_employees/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."companies" add column "employees" integer +-- not null default '1'; diff --git a/hasura/migrations/default/1682514575594_alter_table_public_companies_add_column_employees/up.sql b/hasura/migrations/default/1682514575594_alter_table_public_companies_add_column_employees/up.sql new file mode 100644 index 000000000..b2668c1a9 --- /dev/null +++ b/hasura/migrations/default/1682514575594_alter_table_public_companies_add_column_employees/up.sql @@ -0,0 +1,2 @@ +alter table "public"."companies" add column "employees" integer + not null default '1'; diff --git a/hasura/migrations/default/1682514633489_alter_table_public_companies_add_column_address/down.sql b/hasura/migrations/default/1682514633489_alter_table_public_companies_add_column_address/down.sql new file mode 100644 index 000000000..f4c1d00f8 --- /dev/null +++ b/hasura/migrations/default/1682514633489_alter_table_public_companies_add_column_address/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."companies" add column "address" text +-- null; diff --git a/hasura/migrations/default/1682514633489_alter_table_public_companies_add_column_address/up.sql b/hasura/migrations/default/1682514633489_alter_table_public_companies_add_column_address/up.sql new file mode 100644 index 000000000..4fe769a65 --- /dev/null +++ b/hasura/migrations/default/1682514633489_alter_table_public_companies_add_column_address/up.sql @@ -0,0 +1,2 @@ +alter table "public"."companies" add column "address" text + null; diff --git a/hasura/migrations/default/1682514786385_set_fk_public_companies_account_owner_id/down.sql b/hasura/migrations/default/1682514786385_set_fk_public_companies_account_owner_id/down.sql new file mode 100644 index 000000000..570caab7e --- /dev/null +++ b/hasura/migrations/default/1682514786385_set_fk_public_companies_account_owner_id/down.sql @@ -0,0 +1 @@ +alter table "public"."companies" drop constraint "companies_account_owner_id_fkey"; diff --git a/hasura/migrations/default/1682514786385_set_fk_public_companies_account_owner_id/up.sql b/hasura/migrations/default/1682514786385_set_fk_public_companies_account_owner_id/up.sql new file mode 100644 index 000000000..c807c48df --- /dev/null +++ b/hasura/migrations/default/1682514786385_set_fk_public_companies_account_owner_id/up.sql @@ -0,0 +1,5 @@ +alter table "public"."companies" + add constraint "companies_account_owner_id_fkey" + foreign key ("account_owner_id") + references "auth"."users" + ("id") on update set null on delete set null;