diff --git a/packages/server/src/api/controllers/Organization.ts b/packages/server/src/api/controllers/Organization.ts index 9d041f9d1..2124b76fe 100644 --- a/packages/server/src/api/controllers/Organization.ts +++ b/packages/server/src/api/controllers/Organization.ts @@ -18,7 +18,7 @@ import BaseController from '@/api/controllers/BaseController'; @Service() export default class OrganizationController extends BaseController { @Inject() - organizationService: OrganizationService; + private organizationService: OrganizationService; /** * Router constructor. @@ -69,6 +69,19 @@ export default class OrganizationController extends BaseController { check('fiscal_year').exists().isIn(MONTHS), check('language').exists().isString().isIn(ACCEPTED_LOCALES), check('date_format').optional().isIn(DATE_FORMATS), + + // # Address + check('address').optional({ nullable: true }), + check('address.address_1').optional().isString().trim(), + check('address.address_2').optional().isString().trim(), + check('address.postal_code').optional().isString().trim(), + check('address.city').optional().isString().trim(), + check('address.state_province').optional().isString().trim(), + check('address.phone').optional().isString().trim(), + + // # Branding + check('primary_color').optional().isString().trim(), + check('company_logo_key').optional().isString().trim(), ]; } @@ -86,7 +99,28 @@ export default class OrganizationController extends BaseController { */ private get updateOrganizationValidationSchema(): ValidationChain[] { return [ - ...this.commonOrganizationValidationSchema, + // # Profile + check('name').optional().trim(), + check('industry').optional({ nullable: true }).isString().trim(), + check('location').optional().isString().isISO31661Alpha2(), + check('base_currency').optional().isISO4217(), + check('timezone').optional().isIn(moment.tz.names()), + check('fiscal_year').optional().isIn(MONTHS), + check('language').optional().isString().isIn(ACCEPTED_LOCALES), + check('date_format').optional().isIn(DATE_FORMATS), + + // # Address + check('address.address_1').optional().isString().trim(), + check('address.address_2').optional().isString().trim(), + check('address.postal_code').optional().isString().trim(), + check('address.city').optional().isString().trim(), + check('address.state_province').optional().isString().trim(), + check('address.phone').optional().isString().trim(), + + // # Branding + check('primary_color').optional().isHexColor().trim(), + check('logo_key').optional().isString().trim(), + check('tax_number').optional({ nullable: true }).isString().trim(), ]; } @@ -156,7 +190,7 @@ export default class OrganizationController extends BaseController { next: NextFunction ) { const { tenantId } = req; - const tenantDTO = this.matchedBodyData(req); + const tenantDTO = this.matchedBodyData(req, { includeOptionals: false }); try { await this.organizationService.updateOrganization(tenantId, tenantDTO); diff --git a/packages/server/src/interfaces/Setup.ts b/packages/server/src/interfaces/Setup.ts index da776b89e..32c8fe97b 100644 --- a/packages/server/src/interfaces/Setup.ts +++ b/packages/server/src/interfaces/Setup.ts @@ -18,14 +18,26 @@ export interface IOrganizationBuildDTO { dateFormat?: string; } +interface OrganizationAddressDTO { + address1: string; + address2: string; + postalCode: string; + city: string; + stateProvince: string; + phone: string; +} + export interface IOrganizationUpdateDTO { name: string; - location: string; - baseCurrency: string; - timezone: string; - fiscalYear: string; - industry: string; - taxNumber: string; + location?: string; + baseCurrency?: string; + timezone?: string; + fiscalYear?: string; + industry?: string; + taxNumber?: string; + primaryColor?: string; + logoKey?: string; + address?: OrganizationAddressDTO; } export interface IOrganizationBuildEventPayload { @@ -36,4 +48,4 @@ export interface IOrganizationBuildEventPayload { export interface IOrganizationBuiltEventPayload { tenantId: number; -} \ No newline at end of file +} diff --git a/packages/server/src/services/Organization/OrganizationService.ts b/packages/server/src/services/Organization/OrganizationService.ts index 6d7fa1621..67c9656c8 100644 --- a/packages/server/src/services/Organization/OrganizationService.ts +++ b/packages/server/src/services/Organization/OrganizationService.ts @@ -93,7 +93,7 @@ export default class OrganizationService { // Triggers the organization built event. await this.eventPublisher.emitAsync(events.organization.built, { tenantId: tenant.id, - } as IOrganizationBuiltEventPayload) + } as IOrganizationBuiltEventPayload); } /** @@ -190,11 +190,13 @@ export default class OrganizationService { this.throwIfTenantNotExists(tenant); // Validate organization transactions before mutate base currency. - await this.validateMutateBaseCurrency( - tenant, - organizationDTO.baseCurrency, - tenant.metadata?.baseCurrency - ); + if (organizationDTO.baseCurrency) { + await this.validateMutateBaseCurrency( + tenant, + organizationDTO.baseCurrency, + tenant.metadata?.baseCurrency + ); + } await tenant.saveMetadata(organizationDTO); if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) { diff --git a/packages/server/src/services/Organization/OrganizationUpgrade.ts b/packages/server/src/services/Organization/OrganizationUpgrade.ts index f8b6f7b57..9155539aa 100644 --- a/packages/server/src/services/Organization/OrganizationUpgrade.ts +++ b/packages/server/src/services/Organization/OrganizationUpgrade.ts @@ -13,16 +13,13 @@ import TenantsManagerService from '@/services/Tenancy/TenantsManager'; @Service() export default class OrganizationUpgrade { @Inject() - tenancy: HasTenancyService; + private organizationService: OrganizationService; @Inject() - organizationService: OrganizationService; - - @Inject() - tenantsManager: TenantsManagerService; + private tenantsManager: TenantsManagerService; @Inject('agenda') - agenda: any; + private agenda: any; /** * Upgrades the given organization database. @@ -102,4 +99,4 @@ export default class OrganizationUpgrade { throw new ServiceError(ERRORS.TENANT_UPGRADE_IS_RUNNING); } } -} \ No newline at end of file +} diff --git a/packages/server/src/system/migrations/20240928145627_add_logo_key_to_tenant_metadata.js b/packages/server/src/system/migrations/20240928145627_add_logo_key_to_tenant_metadata.js new file mode 100644 index 000000000..c1197335b --- /dev/null +++ b/packages/server/src/system/migrations/20240928145627_add_logo_key_to_tenant_metadata.js @@ -0,0 +1,23 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.table('tenants_metadata', (table) => { + table.string('primary_color'); + table.string('logo_key'); + table.json('address'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.table('tenants_metadata', (table) => { + table.dropColumn('primary_color'); + table.dropColumn('logo_key'); + table.dropColumn('address'); + }); +}; diff --git a/packages/server/src/system/models/Tenant.ts b/packages/server/src/system/models/Tenant.ts index bea33bb2d..c91ca2081 100644 --- a/packages/server/src/system/models/Tenant.ts +++ b/packages/server/src/system/models/Tenant.ts @@ -169,7 +169,7 @@ export default class Tenant extends BaseModel { */ static async saveMetadata(tenantId, metadata) { const foundMetadata = await TenantMetadata.query().findOne({ tenantId }); - const updateOrInsert = foundMetadata ? 'update' : 'insert'; + const updateOrInsert = foundMetadata ? 'patch' : 'insert'; return TenantMetadata.query() [updateOrInsert]({ diff --git a/packages/server/src/system/models/TenantMetadata.ts b/packages/server/src/system/models/TenantMetadata.ts index 98953dd43..ccf183c2f 100644 --- a/packages/server/src/system/models/TenantMetadata.ts +++ b/packages/server/src/system/models/TenantMetadata.ts @@ -1,8 +1,39 @@ import BaseModel from 'models/Model'; export default class TenantMetadata extends BaseModel { - baseCurrency: string; - name: string; + baseCurrency!: string; + name!: string; + tenantId!: number; + industry!: string; + location!: string; + language!: string; + timezone!: string; + dateFormat!: string; + fiscalYear!: string; + primaryColor!: string; + logoKey!: string; + address!: object; + + static get jsonSchema() { + return { + type: 'object', + required: ['tenantId', 'name', 'baseCurrency'], + properties: { + tenantId: { type: 'integer' }, + name: { type: 'string', maxLength: 255 }, + industry: { type: 'string', maxLength: 255 }, + location: { type: 'string', maxLength: 255 }, + baseCurrency: { type: 'string', maxLength: 3 }, + language: { type: 'string', maxLength: 255 }, + timezone: { type: 'string', maxLength: 255 }, + dateFormat: { type: 'string', maxLength: 255 }, + fiscalYear: { type: 'string', maxLength: 255 }, + primaryColor: { type: 'string', maxLength: 7 }, // Assuming hex color code + logoKey: { type: 'string', maxLength: 255 }, + address: { type: 'object' }, + }, + }; + } /** * Table name. diff --git a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx index c69d5ad7b..0762b8482 100644 --- a/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx +++ b/packages/webapp/src/containers/Preferences/Branding/PreferencesBrandingFormContent.tsx @@ -8,7 +8,7 @@ import styles from './PreferencesBranding.module.scss'; export function PreferencesBrandingFormContent() { return ( - + diff --git a/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx b/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx index d67d0ea65..4560a0dde 100644 --- a/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx +++ b/packages/webapp/src/containers/Preferences/General/GeneralForm.tsx @@ -14,6 +14,8 @@ import { FFormGroup, FInputGroup, FSelect, + Stack, + Group, } from '@/components'; import { inputIntent } from '@/utils'; import { CLASSES } from '@/constants/classes'; @@ -99,6 +101,50 @@ export default function PreferencesGeneralForm({ isSubmitting }) { /> + {/* ---------- Address ---------- */} + + + + + + + + + + + + + + + + + {/* ---------- Base currency ---------- */}