diff --git a/assets/images/integrationicons/rillet-icon-square.svg b/assets/images/integrationicons/rillet-icon-square.svg new file mode 100644 index 000000000000..b6fa8c029512 --- /dev/null +++ b/assets/images/integrationicons/rillet-icon-square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 52b1994f6fe6..c6670d39dcc0 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -491,6 +491,7 @@ const CONST = { INTACCT: 'Intacct', SAGE_INTACCT: 'Sage Intacct', CERTINIA: 'FinancialForce', + RILLET: 'Rillet', BILLCOM: 'Bill.com', ZENEFITS: 'Zenefits', }, @@ -972,6 +973,7 @@ const CONST = { CERTINIA: 'financialForceNewDot', MERGE_HR: 'mergeHRConnections', VENDOR_MATCHING: 'vendorMatching', + RILLET: 'rillet', RULES_REVAMP: 'rulesRevamp', COMMUTER_EXCLUSIONS: 'commuterExclusions', }, @@ -3330,6 +3332,34 @@ const CONST = { VENDOR_BILL: 'VENDOR_BILL', }, + RILLET_CONFIG: { + SUBSIDIARY_ID: 'subsidiaryID', + ENABLE_NEW_CATEGORIES: 'enableNewCategories', + SYNC_TAX_RATES: 'syncTaxRates', + EXPORTER: 'exporter', + EXPORT_DATE: 'exportDate', + REIMBURSABLE: 'reimbursable', + COMPANY_CARD: 'companyCard', + DEFAULT_VENDORID: 'defaultVendorID', + CREDIT_CARD_ACCOUNTCODE: 'creditCardAccountCode', + EXPORT_TO_MULTIPLE_ACCOUNTS: 'exportToMultipleAccounts', + CARD_PROGRAM_ACCOUNTS: 'cardProgramAccounts', + ACCOUNTING_METHOD: 'accountingMethod', + AUTO_SYNC: 'autoSync', + SYNC_REIMBURSED_REPORTS: 'syncReimbursedReports', + BILL_PAYMENT_ACCOUNT_CODE: 'billPaymentAccountCode', + SYNC_EXPENSIFY_CARD_SETTLEMENTS: 'syncExpensifyCardSettlements', + SETTLEMENTS_BANK_ACCOUNT_ID: 'settlementsBankAccountID', + SYNC_TRAVEL_INVOICING_SETTLEMENTS: 'syncTravelInvoicingSettlements', + TRAVEL_INVOICING_SETTLEMENTS_BANK_ACCOUNT_ID: 'travelInvoicingSettlementsBankAccountID', + FIELD_MAPPING_PREFIX: 'fieldMapping_', + }, + + RILLET_MAPPING_VALUE: { + NONE: 'NONE', + TAG: 'TAG', + }, + UPDATE_PERSONAL_BANK_ACCOUNT: { PAGE_NAME: { LEGAL_NAME: 'legal-name', @@ -3985,6 +4015,7 @@ const CONST = { NETSUITE: 'netsuite', SAGE_INTACCT: 'intacct', CERTINIA: 'financialforce', + RILLET: 'rillet', GUSTO: 'gusto', ZENEFITS: 'zenefits', MERGE_HR: 'merge_hris', @@ -4002,6 +4033,7 @@ const CONST = { SAGE_INTACCT: 'sage-intacct', QBD: 'quickbooks-desktop', CERTINIA: 'certinia', + RILLET: 'rillet', GUSTO: 'gusto', ZENEFITS: 'zenefits', MERGE_HR: 'merge-hr', @@ -4013,6 +4045,7 @@ const CONST = { xero: 'Xero', intacct: 'Sage Intacct', financialforce: 'Certinia', + rillet: 'Rillet', gusto: 'Gusto', billCom: 'Bill.com', zenefits: 'TriNet', @@ -4023,7 +4056,7 @@ const CONST = { other: 'Other', }, get ACCOUNTING_CONNECTION_NAMES() { - return [this.NAME.QBO, this.NAME.QBD, this.NAME.XERO, this.NAME.NETSUITE, this.NAME.SAGE_INTACCT, this.NAME.CERTINIA] as const; + return [this.NAME.QBO, this.NAME.QBD, this.NAME.XERO, this.NAME.NETSUITE, this.NAME.SAGE_INTACCT, this.NAME.CERTINIA, this.NAME.RILLET] as const; }, get HR_CONNECTION_NAMES() { return [this.NAME.GUSTO, this.NAME.ZENEFITS, this.NAME.MERGE_HR] as const; @@ -4123,6 +4156,9 @@ const CONST = { FINANCIAL_FORCE_SYNC_USERS: 'financialForceSyncUsers', FINANCIAL_FORCE_SYNC_DIMENSIONS: 'financialForceSyncDimensions', FINANCIAL_FORCE_SYNC_MARK_REIMBURSED: 'financialForceMarkAsReimbursed', + RILLET_SYNC_TITLE: 'rilletSyncTitle', + RILLET_SYNC_CONNECTION: 'rilletSyncConnection', + RILLET_SYNC_IMPORT_DATA: 'rilletSyncImportData', }, SYNC_STAGE_TIMEOUT_MINUTES: 20, }, @@ -7216,6 +7252,14 @@ const CONST = { description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.CERTINIA}.description` as const, icon: 'CertiniaSquare', }, + [this.POLICY.CONNECTIONS.NAME.RILLET]: { + id: this.POLICY.CONNECTIONS.NAME.RILLET, + alias: 'rillet', + name: this.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.rillet, + title: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.RILLET}.title` as const, + description: `workspace.upgrade.${this.POLICY.CONNECTIONS.NAME.RILLET}.description` as const, + icon: 'RilletSquare', + }, approvals: { id: 'approvals' as const, alias: 'approvals' as const, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 46547b6e7d86..a49bb90b8a81 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1185,6 +1185,8 @@ const ONYXKEYS = { ADD_AGENT_RULE_FORM_DRAFT: 'addAgentRuleFormDraft', EDIT_AGENT_RULE_FORM: 'editAgentRuleForm', EDIT_AGENT_RULE_FORM_DRAFT: 'editAgentRuleFormDraft', + RILLET_CREDENTIALS_FORM: 'rilletCredentialsForm', + RILLET_CREDENTIALS_FORM_DRAFT: 'rilletCredentialsFormDraft', }, DERIVED: { REPORT_ATTRIBUTES: 'reportAttributes', @@ -1330,6 +1332,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.EDIT_AGENT_PROMPT_FORM]: FormTypes.EditAgentPromptForm; [ONYXKEYS.FORMS.ADD_AGENT_RULE_FORM]: FormTypes.AddAgentRuleForm; [ONYXKEYS.FORMS.EDIT_AGENT_RULE_FORM]: FormTypes.EditAgentRuleForm; + [ONYXKEYS.FORMS.RILLET_CREDENTIALS_FORM]: FormTypes.RilletCredentialsForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 298d3780cc1a..b6ac99b830b1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3985,6 +3985,22 @@ const ROUTES = { return `workspaces/${policyID}/accounting/certinia/company` as const; }, }, + POLICY_ACCOUNTING_RILLET_SETUP: { + route: 'workspaces/:policyID/accounting/rillet/setup', + getRoute: (policyID: string) => `workspaces/${policyID}/accounting/rillet/setup` as const, + }, + POLICY_ACCOUNTING_RILLET_EXISTING_CONNECTIONS: { + route: 'workspaces/:policyID/accounting/rillet/existing-connections', + getRoute: (policyID: string) => `workspaces/${policyID}/accounting/rillet/existing-connections` as const, + }, + POLICY_ACCOUNTING_RILLET_SUBSIDIARY_SELECTOR: { + route: 'workspaces/:policyID/accounting/rillet/subsidiary-selector', + getRoute: (policyID: string) => `workspaces/${policyID}/accounting/rillet/subsidiary-selector` as const, + }, + POLICY_ACCOUNTING_RILLET_IMPORT: { + route: 'workspaces/:policyID/accounting/rillet/import', + getRoute: (policyID: string) => `workspaces/${policyID}/accounting/rillet/import` as const, + }, ADD_EXISTING_EXPENSE: { route: 'search/r/:reportID/add-existing-expense/:backToReport?', getRoute: (reportID: string | undefined, backToReport?: string) => `search/r/${reportID}/add-existing-expense/${backToReport ?? ''}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5e5d37be9e0e..c5067157909f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -661,6 +661,10 @@ const SCREENS = { CERTINIA_TAGS_MAPPING: 'Policy_Accounting_Certinia_Tags_Mapping', CERTINIA_REPORT_EXPORT_STATUS: 'Policy_Accounting_Certinia_Report_Export_Status', CERTINIA_COMPANY_SELECTOR: 'Policy_Accounting_Certinia_Company_Selector', + RILLET_SETUP: 'Policy_Accounting_Rillet_Setup', + RILLET_EXISTING_CONNECTIONS: 'Policy_Accounting_Rillet_Existing_Connections', + RILLET_SUBSIDIARY_SELECTOR: 'Policy_Accounting_Rillet_Subsidiary_Selector', + RILLET_IMPORT: 'Policy_Accounting_Rillet_Import', CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation', CARD_RECONCILIATION_SAGE_INTACCT_AUTO_SYNC: 'Policy_Accounting_Card_Reconciliation_Sage_Intacct_Auto_Sync', DYNAMIC_RECONCILIATION_ACCOUNT_SETTINGS: 'Dynamic_Policy_Accounting_Reconciliation_Account_Settings', diff --git a/src/components/ConnectToRilletFlow/index.tsx b/src/components/ConnectToRilletFlow/index.tsx new file mode 100644 index 000000000000..e785af249d45 --- /dev/null +++ b/src/components/ConnectToRilletFlow/index.tsx @@ -0,0 +1,27 @@ +import {useEffect} from 'react'; +import useHasReusablePoliciesConnectedTo from '@hooks/useHasReusablePoliciesConnectedTo'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +type ConnectToRilletFlowProps = { + policyID: string; +}; + +function ConnectToRilletFlow({policyID}: ConnectToRilletFlowProps) { + const hasReusablePoliciesConnectedToRillet = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.RILLET, policyID); + + useEffect(() => { + if (hasReusablePoliciesConnectedToRillet) { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_RILLET_EXISTING_CONNECTIONS.getRoute(policyID)); + return; + } + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_RILLET_SETUP.getRoute(policyID)); + // This needs to run once as we will navigate away + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +} + +export default ConnectToRilletFlow; diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts index e8b33cf83d01..ebe9e8c7d850 100644 --- a/src/components/Icon/chunks/expensify-icons.chunk.ts +++ b/src/components/Icon/chunks/expensify-icons.chunk.ts @@ -145,6 +145,7 @@ import OracleSquare from '@assets/images/integrationicons/oracle-icon-square.svg import QBDSquare from '@assets/images/integrationicons/qbd-icon-square.svg'; import QBOCircle from '@assets/images/integrationicons/qbo-icon-circle.svg'; import QBOSquare from '@assets/images/integrationicons/qbo-icon-square.svg'; +import RilletSquare from '@assets/images/integrationicons/rillet-icon-square.svg'; import SageIntacctSquare from '@assets/images/integrationicons/sage-intacct-icon-square.svg'; import SapSquare from '@assets/images/integrationicons/sap-icon-square.svg'; import TriNetSquare from '@assets/images/integrationicons/trinet-icon-square.svg'; @@ -465,6 +466,7 @@ const Expensicons = { ReportCopy, ReplaceReceipt, ReceiptMultiple, + RilletSquare, Rotate, RotateLeft, Scan, diff --git a/src/components/ImportedFromAccountingSoftware.tsx b/src/components/ImportedFromAccountingSoftware.tsx index 8fabe6997d5a..e6ead39ee07d 100644 --- a/src/components/ImportedFromAccountingSoftware.tsx +++ b/src/components/ImportedFromAccountingSoftware.tsx @@ -41,7 +41,7 @@ function ImportedFromAccountingSoftware({policyID, currentConnectionName, transl const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {environmentURL} = useEnvironment(); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['XeroSquare', 'QBOSquare', 'NetSuiteSquare', 'IntacctSquare', 'QBDSquare', 'CertiniaSquare', 'GustoSquare']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['XeroSquare', 'QBOSquare', 'NetSuiteSquare', 'IntacctSquare', 'QBDSquare', 'CertiniaSquare', 'RilletSquare', 'GustoSquare']); const icon = getIntegrationIcon(connectedIntegration, expensifyIcons); if (!customTagName && shouldShow) { diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx index ee677d23e63d..ee180f37f4a0 100644 --- a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx +++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx @@ -51,7 +51,7 @@ function ExportWithDropdownMenu({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const {showConfirmModal} = useConfirmModal(); const [exportMethods] = useOnyx(ONYXKEYS.LAST_EXPORT_METHOD); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['XeroSquare', 'QBOSquare', 'NetSuiteSquare', 'IntacctSquare', 'QBDSquare', 'CertiniaSquare', 'GustoSquare']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['XeroSquare', 'QBOSquare', 'NetSuiteSquare', 'IntacctSquare', 'QBDSquare', 'CertiniaSquare', 'RilletSquare', 'GustoSquare']); const iconToDisplay = getIntegrationIcon(connectionName, expensifyIcons); const canBeExported = canBeExportedUtils(report); diff --git a/src/components/Search/FilterComponents/ExportedToSelector.tsx b/src/components/Search/FilterComponents/ExportedToSelector.tsx index 2b0fd8bda9f5..19c3648c1226 100644 --- a/src/components/Search/FilterComponents/ExportedToSelector.tsx +++ b/src/components/Search/FilterComponents/ExportedToSelector.tsx @@ -40,6 +40,7 @@ function ExportedToSelector({value = [], policyIDs = [], selectionListTextInputS 'IntacctSquare', 'QBDSquare', 'CertiniaSquare', + 'RilletSquare', 'GustoSquare', 'Table', 'TablePencil', diff --git a/src/components/Search/SearchList/ListItem/ExportedIconCell.tsx b/src/components/Search/SearchList/ListItem/ExportedIconCell.tsx index 0f7b44ce5791..facd30138001 100644 --- a/src/components/Search/SearchList/ListItem/ExportedIconCell.tsx +++ b/src/components/Search/SearchList/ListItem/ExportedIconCell.tsx @@ -20,7 +20,18 @@ function ExportedIconCell({reportActions}: ExportedIconCellProps) { const styles = useThemeStyles(); const actions = reportActions ?? []; - const icons = useMemoizedLazyExpensifyIcons(['NetSuiteSquare', 'XeroSquare', 'IntacctSquare', 'QBOSquare', 'Table', 'TablePencil', 'ZenefitsSquare', 'BillComSquare', 'CertiniaSquare']); + const icons = useMemoizedLazyExpensifyIcons([ + 'NetSuiteSquare', + 'XeroSquare', + 'IntacctSquare', + 'QBOSquare', + 'Table', + 'TablePencil', + 'ZenefitsSquare', + 'BillComSquare', + 'CertiniaSquare', + 'RilletSquare', + ]); let isExportedToStandardTemplate = false; let isExportedToCustomTemplate = false; @@ -30,6 +41,7 @@ function ExportedIconCell({reportActions}: ExportedIconCellProps) { let isExportedToQuickbooksOnline = false; let isExportedToQuickbooksDesktop = false; let isExportedToCertinia = false; + let isExportedToRillet = false; let isExportedToBillCom = false; let isExportedToZenefits = false; @@ -58,6 +70,7 @@ function ExportedIconCell({reportActions}: ExportedIconCellProps) { isExportedToZenefits = isExportedToZenefits || label === CONST.EXPORT_LABELS.ZENEFITS; isExportedToBillCom = isExportedToBillCom || label === CONST.EXPORT_LABELS.BILLCOM; isExportedToCertinia = isExportedToCertinia || label === CONST.EXPORT_LABELS.CERTINIA; + isExportedToRillet = isExportedToRillet || label === CONST.EXPORT_LABELS.RILLET; isExportedToIntacct = isExportedToIntacct || label === CONST.EXPORT_LABELS.INTACCT || label === CONST.EXPORT_LABELS.SAGE_INTACCT; } } @@ -113,6 +126,13 @@ function ExportedIconCell({reportActions}: ExportedIconCellProps) { size={CONST.AVATAR_SIZE.MID_SUBSCRIPT} /> )} + {isExportedToRillet && ( + + )} {isExportedToBillCom && ( = { editor: 'Editor', restrictions: 'Beschränkungen', off: 'Aus', + apiKey: 'API-Schlüssel', }, socials: { podcast: 'Folgen Sie uns auf Podcast', @@ -5495,6 +5496,16 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU } }, }, + rillet: { + rilletSetup: 'Rillet-Einrichtung', + enterCredentials: 'Geben Sie Ihren Rillet-API-Schlüssel ein', + howToFindAPIKey: + 'So finden Sie Ihren API-Schlüssel.
  1. Melden Sie sich bei Rillet an
  2. Navigieren Sie zu Konto -> Einstellungen
  3. Kopieren Sie den unten stehenden API-Schlüssel
', + subsidiary: 'Tochtergesellschaft', + subsidiarySelectDescription: 'Wählen Sie die Tochtergesellschaft in Rillet aus, aus der Sie Daten importieren möchten.', + noSubsidiariesFound: 'Keine Tochtergesellschaften gefunden', + noSubsidiariesFoundDescription: 'Bitte fügen Sie eine Tochtergesellschaft in Rillet hinzu und synchronisieren Sie die Verbindung erneut.', + }, type: { free: 'Kostenlos', control: 'Steuerung', @@ -6461,6 +6472,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6478,6 +6490,8 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6703,6 +6717,12 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU return 'Dimensionen werden importiert'; case 'financialForceMarkAsReimbursed': return 'Berichte werden als erstattet markiert'; + case 'rilletSyncTitle': + return 'Rillet-Daten werden synchronisiert'; + case 'rilletSyncConnection': + return 'Verbindung mit Rillet wird initialisiert'; + case 'rilletSyncImportData': + return 'Daten werden geladen'; default: { return `Übersetzung fehlt für Stufe: ${stage}`; } @@ -6973,6 +6993,12 @@ ${reportName}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Unsere Certinia-Integration ist nur im Control-Tarif verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `Profitiere von automatisierter Synchronisierung und reduziere manuelle Eingaben mit der Expensify + Rillet-Integration. Richte Spesenkodierungsdimensionen und die Steuersynchronisierung auf deine Rillet-Einrichtung aus, um eine klarere finanzielle Übersicht zu erhalten.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Unsere Rillet-Integration ist nur im Control-Tarif verfügbar, beginnend bei ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: 'Erweiterte Genehmigungen', description: `Wenn du weitere Genehmigungsstufen hinzufügen möchtest – oder einfach sicherstellen willst, dass die höchsten Ausgaben noch einmal geprüft werden – bist du bei uns richtig. Erweiterte Genehmigungen helfen dir, auf jeder Ebene die passenden Kontrollen einzurichten, damit du die Ausgaben deines Teams im Griff behältst.`, diff --git a/src/languages/en.ts b/src/languages/en.ts index e1b8fb07ffa3..ab6c9a545b7b 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -536,6 +536,7 @@ const translations = { goToConcierge: 'Go to Concierge', allSet: 'All Set!', enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `enter digit ${digitIndex} of ${totalDigits}`, + apiKey: 'API key', }, socials: { podcast: 'Follow us on Podcast', @@ -5553,6 +5554,20 @@ const translations = { } }, }, + rillet: { + rilletSetup: 'Rillet setup', + enterCredentials: 'Enter your Rillet API key', + howToFindAPIKey: 'Finding your API key.
  1. Log in to Rillet
  2. Navigate to Account -> Settings
  3. Copy the API key below
', + subsidiary: 'Subsidiary', + subsidiarySelectDescription: "Choose the subsidiary in Rillet that you'd like to import data from.", + noSubsidiariesFound: 'No subsidiaries found', + noSubsidiariesFoundDescription: 'Please add a subsidiary in Rillet and sync the connection again', + accountTypesDescription: 'Your Rillet accounts will import as categories.', + enableNewAccountsTitle: 'Enable newly imported accounts', + enableNewAccountsDescription: 'New Rillet accounts will be available as categories.', + dimensionsImport: 'All Rillet dimensions import as tags', + importDescription: 'Choose which coding configurations to import from Rillet.', + }, type: { free: 'Free', control: 'Control', @@ -6276,6 +6291,7 @@ const translations = { value: 'Value', taxReclaimableOn: 'Tax reclaimable on', taxRate: 'Tax rate', + taxRates: 'Tax rates', findTaxRate: 'Find tax rate', error: { taxRateAlreadyExists: 'This tax name is already in use', @@ -6521,6 +6537,7 @@ const translations = { xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6538,6 +6555,8 @@ const translations = { return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6766,6 +6785,12 @@ const translations = { return 'Importing dimensions'; case 'financialForceMarkAsReimbursed': return 'Marking reports as reimbursed'; + case 'rilletSyncTitle': + return 'Syncing Rillet data'; + case 'rilletSyncConnection': + return 'Initializing connection to Rillet'; + case 'rilletSyncImportData': + return 'Loading data'; default: { return `Translation missing for stage: ${stage}`; } @@ -7110,6 +7135,12 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Our Certinia integration is only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `Enjoy automated syncing and reduce manual entries with the Expensify + Rillet integration. Align expense coding dimensions and tax sync with your Rillet setup for clearer financial visibility.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Our Rillet integration is only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: 'Advanced Approvals', description: `If you want to add more layers of approval to the mix – or just make sure the largest expenses get another set of eyes – we’ve got you covered. Advanced approvals help you put the right checks in place at every level so you keep your team’s spend under control.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index f8e09a6398d1..7d2062d5a8bb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -463,6 +463,7 @@ const translations: TranslationDeepObject = { goToConcierge: 'Ir a Concierge', allSet: '¡Todo listo!', enterDigitLabel: ({digitIndex, totalDigits}: {digitIndex: number; totalDigits: number}) => `introducir dígito ${digitIndex} de ${totalDigits}`, + apiKey: 'Clave API', editor: 'Editor', restrictions: 'Restricciones', off: 'Desactivado', @@ -5336,6 +5337,16 @@ ${amount} para ${merchant} - ${date}`, } }, }, + rillet: { + rilletSetup: 'Configuración de Rillet', + enterCredentials: 'Introduce tu clave de API de Rillet', + howToFindAPIKey: + 'Encontrar tu clave de API.
  1. Inicia sesión en Rillet
  2. Ve a Cuenta -> Configuración
  3. Copia la clave de API de abajo
', + subsidiary: 'Filial', + subsidiarySelectDescription: 'Elige la filial en Rillet desde la que te gustaría importar datos.', + noSubsidiariesFound: 'No se encontraron filiales', + noSubsidiariesFoundDescription: 'Por favor, añade una filial en Rillet y sincroniza de nuevo la conexión', + }, type: { free: 'Gratis', control: 'Controlar', @@ -6233,6 +6244,7 @@ ${amount} para ${merchant} - ${date}`, xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6250,6 +6262,8 @@ ${amount} para ${merchant} - ${date}`, return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6476,6 +6490,12 @@ ${amount} para ${merchant} - ${date}`, return 'Importando dimensiones'; case 'financialForceMarkAsReimbursed': return 'Marcando informes como reembolsados'; + case 'rilletSyncTitle': + return 'Sincronizando datos de Rillet'; + case 'rilletSyncConnection': + return 'Iniciando conexión con Rillet'; + case 'rilletSyncImportData': + return 'Cargando datos'; default: { return `Translation missing for stage: ${stage}`; } @@ -6899,6 +6919,12 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Nuestra integración con Certinia solo está disponible en el plan Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `Disfruta de la sincronización automatizada y reduce las entradas manuales con la integración Expensify + Rillet. Alinea dimensiones de codificación de gastos y la sincronización de impuestos con tu configuración de Rillet para una visibilidad financiera más clara.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `Nuestra integración con Rillet solo está disponible en el plan Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: 'Aprobaciones anticipadas', description: `Si quieres añadir más niveles de aprobación, o simplemente asegurarte de que los gastos más importantes reciben otro vistazo, no hay problema. Las aprobaciones avanzadas ayudan a realizar las comprobaciones adecuadas a cada nivel para mantener los gastos de tu equipo bajo control.`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 88fa4ebcae0d..28d8d23e2cb3 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -515,6 +515,7 @@ const translations: TranslationDeepObject = { editor: 'Éditeur', restrictions: 'Restrictions', off: 'Désactivé', + apiKey: 'Clé API', }, socials: { podcast: 'Suivez-nous sur Podcast', @@ -5507,6 +5508,16 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. } }, }, + rillet: { + rilletSetup: 'Configuration Rillet', + enterCredentials: 'Saisissez votre clé API Rillet', + howToFindAPIKey: + 'Recherche de votre clé API.
  1. Connectez-vous à Rillet
  2. Accédez à Compte -> Paramètres
  3. Copiez la clé API ci-dessous
', + subsidiary: 'Filiale', + subsidiarySelectDescription: 'Choisissez la filiale dans Rillet depuis laquelle vous souhaitez importer des données.', + noSubsidiariesFound: 'Aucune filiale trouvée', + noSubsidiariesFoundDescription: 'Veuillez ajouter une filiale dans Rillet et synchroniser à nouveau la connexion', + }, type: { free: 'Gratuit', control: 'Contrôle', @@ -6486,6 +6497,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6503,6 +6515,8 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6729,6 +6743,12 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. return 'Importation des dimensions'; case 'financialForceMarkAsReimbursed': return 'Marquage des notes de frais comme remboursées'; + case 'rilletSyncTitle': + return 'Synchronisation des données Rillet'; + case 'rilletSyncConnection': + return 'Initialisation de la connexion à Rillet'; + case 'rilletSyncImportData': + return 'Chargement des données'; default: { return `Traduction manquante pour l’étape : ${stage}`; } @@ -6999,6 +7019,12 @@ ${reportName}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Notre intégration Certinia est disponible uniquement avec l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `Profitez de la synchronisation automatisée et réduisez les saisies manuelles grâce à l’intégration Expensify + Rillet. Alignez les dimensions de codage des dépenses et la synchronisation fiscale sur votre configuration Rillet pour une meilleure visibilité financière.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Notre intégration Rillet est disponible uniquement avec l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: 'Approbations avancées', description: `Si vous souhaitez ajouter plusieurs niveaux d’approbation au processus – ou simplement vous assurer que les plus grosses dépenses sont examinées une fois de plus – nous avons ce qu’il vous faut. Les approbations avancées vous aident à mettre en place les bons contrôles à chaque niveau afin de garder les dépenses de votre équipe sous contrôle.`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 22f1c24f722a..2d9f270dd16f 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -515,6 +515,7 @@ const translations: TranslationDeepObject = { editor: 'Editor', restrictions: 'Restrizioni', off: 'Disattivato', + apiKey: 'Chiave API', }, socials: { podcast: 'Seguici su Podcast', @@ -5480,6 +5481,15 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. } }, }, + rillet: { + rilletSetup: 'Configurazione Rillet', + enterCredentials: 'Inserisci la tua chiave API Rillet', + howToFindAPIKey: 'Come trovare la tua chiave API.
  1. Accedi a Rillet
  2. Vai su Account -> Impostazioni
  3. Copia la chiave API qui sotto
', + subsidiary: 'Filiale', + subsidiarySelectDescription: 'Scegli la consociata in Rillet da cui vuoi importare i dati.', + noSubsidiariesFound: 'Nessuna consociata trovata', + noSubsidiariesFoundDescription: 'Aggiungi una consociata in Rillet e sincronizza di nuovo la connessione', + }, type: { free: 'Gratis', control: 'Controllo', @@ -6449,6 +6459,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6466,6 +6477,8 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6691,6 +6704,12 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. return 'Importazione delle dimensioni'; case 'financialForceMarkAsReimbursed': return 'Contrassegno dei report come rimborsati'; + case 'rilletSyncTitle': + return 'Sincronizzazione dei dati Rillet'; + case 'rilletSyncConnection': + return 'Inizializzazione della connessione a Rillet'; + case 'rilletSyncImportData': + return 'Caricamento dati'; default: { return `Traduzione mancante per lo stato: ${stage}`; } @@ -6957,6 +6976,12 @@ ${reportName}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `La nostra integrazione con Certinia è disponibile solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`}`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `Approfitta della sincronizzazione automatizzata e riduci le registrazioni manuali con l’integrazione Expensify + Rillet. Allinea dimensioni di codifica delle spese e sincronizzazione fiscale alla tua configurazione Rillet per una maggiore visibilità finanziaria.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `La nostra integrazione con Rillet è disponibile solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`}`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: 'Approvazioni avanzate', description: `Se desideri aggiungere altri livelli di approvazione al processo – o semplicemente assicurarti che le spese più elevate ricevano un ulteriore controllo – ci pensiamo noi. Le approvazioni avanzate ti aiutano a impostare i controlli giusti a ogni livello, così mantieni la spesa del tuo team sotto controllo.`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index d9d7f87120e2..18aa5317190a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -514,6 +514,7 @@ const translations: TranslationDeepObject = { editor: '編集者', restrictions: '制限', off: 'オフ', + apiKey: 'API キー', }, socials: { podcast: 'ポッドキャストでフォロー', @@ -5429,6 +5430,16 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO } }, }, + rillet: { + rilletSetup: 'Rillet のセットアップ', + enterCredentials: 'Rillet の API キーを入力してください', + howToFindAPIKey: + 'API キーの確認方法
  1. Rillet にログインします
  2. [Account]→[Settings]に移動します
  3. 以下の API キーをコピーします
', + subsidiary: '子会社', + subsidiarySelectDescription: 'データをインポートしたい Rillet 内の子会社を選択してください。', + noSubsidiariesFound: '子会社が見つかりません', + noSubsidiariesFoundDescription: 'Rillet に子会社を追加して、もう一度接続を同期してください', + }, type: { free: '無料', control: 'コントロール', @@ -6376,6 +6387,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6393,6 +6405,8 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6618,6 +6632,12 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO return 'ディメンションをインポート中'; case 'financialForceMarkAsReimbursed': return 'レポートを払い戻し済みにマーク中'; + case 'rilletSyncTitle': + return 'Rillet データを同期しています'; + case 'rilletSyncConnection': + return 'Rillet への接続を初期化しています'; + case 'rilletSyncImportData': + return 'データを読み込んでいます'; default: { return `ステージの翻訳が見つかりません: ${stage}`; } @@ -6882,6 +6902,12 @@ ${reportName}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Certinia 連携は Control プランでのみご利用いただけます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からご利用いただけます。`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `Expensify と Rillet の連携で自動同期を活用し、手入力を減らしましょう。経費のコーディングディメンションと税務同期を Rillet の設定に合わせて、財務の可視性を高めます。`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Rillet 連携は Control プランでのみご利用いただけます。${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`} からご利用いただけます。`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: '高度な承認', description: `承認フローにさらに多くの段階を追加したい場合や、高額な経費に必ず別の承認者の目を通したい場合も、ご安心ください。高度な承認機能により、あらゆるレベルで適切なチェック体制を整え、チームの支出をしっかり管理できます。`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 33a50fcd0037..93965976e623 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -514,6 +514,7 @@ const translations: TranslationDeepObject = { editor: 'Editor', restrictions: 'Beperkingen', off: 'Uit', + apiKey: 'API-sleutel', }, socials: { podcast: 'Volg ons op Podcast', @@ -5470,6 +5471,15 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ } }, }, + rillet: { + rilletSetup: 'Rillet-instelling', + enterCredentials: 'Voer je Rillet API-sleutel in', + howToFindAPIKey: 'Je API-sleutel vinden.
  1. Log in bij Rillet
  2. Ga naar Account -> Instellingen
  3. Kopieer de API-sleutel hieronder
', + subsidiary: 'Dochteronderneming', + subsidiarySelectDescription: 'Kies het dochterbedrijf in Rillet waarvan je gegevens wilt importeren.', + noSubsidiariesFound: 'Geen dochterondernemingen gevonden', + noSubsidiariesFoundDescription: 'Voeg alsjeblieft een dochteronderneming toe in Rillet en synchroniseer de verbinding opnieuw', + }, type: { free: 'Gratis', control: 'Beheer', @@ -6431,6 +6441,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6448,6 +6459,8 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6673,6 +6686,12 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ return 'Dimensies importeren'; case 'financialForceMarkAsReimbursed': return 'Rapporten als vergoed markeren'; + case 'rilletSyncTitle': + return 'Rillet-gegevens synchroniseren'; + case 'rilletSyncConnection': + return 'Verbinding met Rillet initialiseren'; + case 'rilletSyncImportData': + return 'Gegevens laden'; default: { return `Vertaling ontbreekt voor fase: ${stage}`; } @@ -6940,6 +6959,12 @@ ${reportName}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Onze Certinia-integratie is alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `Profiteer van automatische synchronisatie en verminder handmatige invoer met de Expensify + Rillet-integratie. Stem uitgavendimensies en belastingsynchronisatie af op je Rillet-configuratie voor helderder financieel inzicht.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Onze Rillet-integratie is alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: 'Geavanceerde goedkeuringen', description: `Als je extra lagen van goedkeuring wilt toevoegen – of gewoon zeker wilt weten dat de hoogste uitgaven een extra controle krijgen – dan ben je bij ons aan het juiste adres. Geavanceerde goedkeuringen helpen je om op elk niveau de juiste controles in te bouwen, zodat je de uitgaven van je team onder controle houdt.`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 5f6f2b1f6a82..d2ca7a1664db 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -514,6 +514,7 @@ const translations: TranslationDeepObject = { editor: 'Edytor', restrictions: 'Ograniczenia', off: 'Wyłączone', + apiKey: 'Klucz API', }, socials: { podcast: 'Śledź nas na Podcast', @@ -5463,6 +5464,15 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy } }, }, + rillet: { + rilletSetup: 'Konfiguracja Rillet', + enterCredentials: 'Wpisz swój klucz API Rillet', + howToFindAPIKey: 'Znajdowanie klucza API.
  1. Zaloguj się do Rillet
  2. Przejdź do Konto -> Ustawienia
  3. Skopiuj poniższy klucz API
', + subsidiary: 'Spółka zależna', + subsidiarySelectDescription: 'Wybierz spółkę zależną w Rillet, z której chcesz zaimportować dane.', + noSubsidiariesFound: 'Nie znaleziono spółek zależnych', + noSubsidiariesFoundDescription: 'Dodaj proszę spółkę zależną w Rillet i ponownie zsynchronizuj połączenie', + }, type: { free: 'Darmowy', control: 'Sterowanie', @@ -6423,6 +6433,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6440,6 +6451,8 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6665,6 +6678,12 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy return 'Importowanie wymiarow'; case 'financialForceMarkAsReimbursed': return 'Oznaczanie raportow jako zwrocone'; + case 'rilletSyncTitle': + return 'Synchronizowanie danych Rillet'; + case 'rilletSyncConnection': + return 'Inicjowanie połączenia z Rillet'; + case 'rilletSyncImportData': + return 'Wczytywanie danych'; default: { return `Brak tłumaczenia dla etapu: ${stage}`; } @@ -6930,6 +6949,12 @@ ${reportName}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Integracja z Certinia jest dostępna tylko w planie Control, zaczynającym się od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`}`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `Korzystaj z automatycznej synchronizacji i ogranicz ręczne wprowadzanie danych dzięki integracji Expensify + Rillet. Dopasuj wymiary kategoryzacji wydatków i synchronizację podatków do konfiguracji Rillet, aby uzyskać lepszą widoczność finansową.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Integracja z Rillet jest dostępna tylko w planie Control, zaczynającym się od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`}`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: 'Zaawansowane zatwierdzanie', description: `Jeśli chcesz dodać więcej poziomów akceptacji – albo po prostu upewnić się, że największe wydatki zostaną przejrzane przez kolejną osobę – mamy na to rozwiązanie. Zaawansowane zatwierdzanie pomaga wdrożyć odpowiednie mechanizmy kontrolne na każdym poziomie, aby utrzymać wydatki zespołu pod kontrolą.`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index f7c967cebe8c..d68a850e6c53 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -513,6 +513,7 @@ const translations: TranslationDeepObject = { editor: 'Editor', restrictions: 'Restrições', off: 'Desligado', + apiKey: 'Chave de API', }, socials: { podcast: 'Siga-nos no Podcast', @@ -5460,6 +5461,16 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS } }, }, + rillet: { + rilletSetup: 'Configuração do Rillet', + enterCredentials: 'Insira sua chave de API Rillet', + howToFindAPIKey: + 'Encontrando sua chave de API.
  1. Faça login no Rillet
  2. Vá para Conta -> Configurações
  3. Copie a chave de API abaixo
', + subsidiary: 'Subsidiária', + subsidiarySelectDescription: 'Escolha a subsidiária no Rillet da qual você gostaria de importar dados.', + noSubsidiariesFound: 'Nenhuma subsidiária encontrada', + noSubsidiariesFoundDescription: 'Adicione uma subsidiária no Rillet e sincronize a conexão novamente', + }, type: { free: 'Grátis', control: 'Controle', @@ -6423,6 +6434,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6440,6 +6452,8 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6665,6 +6679,12 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS return 'Importando dimensoes'; case 'financialForceMarkAsReimbursed': return 'Marcando relatorios como reembolsados'; + case 'rilletSyncTitle': + return 'Sincronizando dados do Rillet'; + case 'rilletSyncConnection': + return 'Inicializando conexão com Rillet'; + case 'rilletSyncImportData': + return 'Carregando dados'; default: { return `Tradução ausente para o estágio: ${stage}`; } @@ -6931,6 +6951,12 @@ ${reportName}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Nossa integração com a Certinia está disponível apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `Aproveite a sincronização automática e reduza lançamentos manuais com a integração Expensify + Rillet. Alinhe dimensões de categorização de despesas e a sincronização de impostos à sua configuração Rillet para maior visibilidade financeira.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Nossa integração com a Rillet está disponível apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: 'Aprovações Avançadas', description: `Se você quiser adicionar mais camadas de aprovação ao processo – ou apenas garantir que as maiores despesas recebam uma segunda revisão – nós ajudamos você. As aprovações avançadas ajudam a colocar os controles certos em cada nível, para manter os gastos da sua equipe sob controle.`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 4ad48b461da4..64b947b9afaf 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -510,6 +510,7 @@ const translations: TranslationDeepObject = { editor: '编辑', restrictions: '限制', off: '关', + apiKey: 'API 密钥', }, socials: { podcast: '在播客上关注我们', @@ -5324,6 +5325,15 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM } }, }, + rillet: { + rilletSetup: 'Rillet 设置', + enterCredentials: '输入你的 Rillet API 密钥', + howToFindAPIKey: '查找您的 API 密钥。
  1. 登录 Rillet
  2. 前往“账号”->“设置”
  3. 复制下面的 API 密钥
', + subsidiary: '子公司', + subsidiarySelectDescription: '请选择要从中导入数据的 Rillet 子公司。', + noSubsidiariesFound: '未找到子公司', + noSubsidiariesFoundDescription: '请在 Rillet 中添加一个子公司,然后再次同步连接', + }, type: { free: '免费', control: '控制', @@ -6253,6 +6263,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM xero: 'Xero', netsuite: 'NetSuite', intacct: 'Sage Intacct', + rillet: 'Rillet', sap: 'SAP', oracle: 'Oracle', microsoftDynamics: 'Microsoft Dynamics', @@ -6270,6 +6281,8 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM return 'NetSuite'; case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: return 'Sage Intacct'; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return 'Rillet'; default: { return ''; } @@ -6493,6 +6506,12 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM return '正在导入维度'; case 'financialForceMarkAsReimbursed': return '正在将报告标记为已报销'; + case 'rilletSyncTitle': + return '正在同步 Rillet 数据'; + case 'rilletSyncConnection': + return '正在初始化与 Rillet 的连接'; + case 'rilletSyncImportData': + return '正在加载数据'; default: { return `缺少以下阶段的翻译:${stage}`; } @@ -6751,6 +6770,12 @@ ${reportName}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `我们的 Certinia 集成仅适用于 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + title: 'Rillet', + description: `通过 Expensify 与 Rillet 的集成,享受自动同步,减少手动录入。将费用编码维度与税务同步与您的 Rillet 配置对齐,以获得更清晰的财务可见性。`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `我们的 Rillet 集成仅适用于 Control 方案,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.id]: { title: '高级审批', description: `如果你想在审批流程中增加更多层级,或者只是想确保金额最大的报销能再多一道审核,我们都能满足你的需求。高级审批功能帮助你在各个层级设置合适的审批规则,从而有效控制团队支出。`, diff --git a/src/libs/API/parameters/ConnectPolicyToRilletParams.ts b/src/libs/API/parameters/ConnectPolicyToRilletParams.ts new file mode 100644 index 000000000000..d98c4b02b2ee --- /dev/null +++ b/src/libs/API/parameters/ConnectPolicyToRilletParams.ts @@ -0,0 +1,6 @@ +type ConnectPolicyToRilletParams = { + policyID: string; + apiKey: string; +}; + +export default ConnectPolicyToRilletParams; diff --git a/src/libs/API/parameters/SyncPolicyToRilletParams.ts b/src/libs/API/parameters/SyncPolicyToRilletParams.ts new file mode 100644 index 000000000000..9d2f533c8fe3 --- /dev/null +++ b/src/libs/API/parameters/SyncPolicyToRilletParams.ts @@ -0,0 +1,6 @@ +type SyncPolicyToRilletParams = { + policyID: string; + idempotencyKey: string; +}; + +export default SyncPolicyToRilletParams; diff --git a/src/libs/API/parameters/UpdateRilletFieldMappingParams.ts b/src/libs/API/parameters/UpdateRilletFieldMappingParams.ts new file mode 100644 index 000000000000..e436e8b2218a --- /dev/null +++ b/src/libs/API/parameters/UpdateRilletFieldMappingParams.ts @@ -0,0 +1,10 @@ +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; + +type UpdateRilletFieldMappingParams = { + policyID: string; + fieldID: string; + mapping: ValueOf; +}; + +export default UpdateRilletFieldMappingParams; diff --git a/src/libs/API/parameters/UpdateRilletGenericTypeParams.ts b/src/libs/API/parameters/UpdateRilletGenericTypeParams.ts new file mode 100644 index 000000000000..ec41d81cbf48 --- /dev/null +++ b/src/libs/API/parameters/UpdateRilletGenericTypeParams.ts @@ -0,0 +1,7 @@ +type UpdateRilletGenericTypeParams = { + policyID: string; + settingValue: string; + idempotencyKey: string; +}; + +export default UpdateRilletGenericTypeParams; diff --git a/src/libs/API/parameters/UpdateRilletSubsidiaryParams.ts b/src/libs/API/parameters/UpdateRilletSubsidiaryParams.ts new file mode 100644 index 000000000000..b313d579e48c --- /dev/null +++ b/src/libs/API/parameters/UpdateRilletSubsidiaryParams.ts @@ -0,0 +1,6 @@ +type UpdateRilletSubsidiaryParams = { + policyID: string; + subsidiaryID: string; +}; + +export default UpdateRilletSubsidiaryParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 75499c213730..e08a41bfecc2 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -24,6 +24,7 @@ export type {default as ConnectPolicyToAccountingIntegrationParams} from './Conn export type {default as ConnectPolicyToGustoParams} from './ConnectPolicyToGustoParams'; export type {default as ConnectPolicyToMergeParams} from './ConnectPolicyToMergeParams'; export type {default as ConnectPolicyToZenefitsParams} from './ConnectPolicyToZenefitsParams'; +export type {default as ConnectPolicyToRilletParams} from './ConnectPolicyToRilletParams'; export type {default as OpenPolicyProfilePageParams} from './OpenPolicyProfilePageParams'; export type {default as OpenPolicyInitialPageParams} from './OpenPolicyInitialPageParams'; export type {default as SyncPolicyToGustoParams} from './SyncPolicyToGustoParams'; @@ -33,6 +34,10 @@ export type {default as SyncPolicyToFinancialForceParams} from './SyncPolicyToFi export type {default as SyncPolicyToQuickbooksOnlineParams} from './SyncPolicyToQuickbooksOnlineParams'; export type {default as SyncPolicyToXeroParams} from './SyncPolicyToXeroParams'; export type {default as SyncPolicyToNetSuiteParams} from './SyncPolicyToNetSuiteParams'; +export type {default as SyncPolicyToRilletParams} from './SyncPolicyToRilletParams'; +export type {default as UpdateRilletSubsidiaryParams} from './UpdateRilletSubsidiaryParams'; +export type {default as UpdateRilletGenericTypeParams} from './UpdateRilletGenericTypeParams'; +export type {default as UpdateRilletFieldMappingParams} from './UpdateRilletFieldMappingParams'; export type {default as UpdateNetSuiteAccountingMethodParams} from './UpdateNetSuiteAccountingMethodParams'; export type {default as UpdateQuickbooksOnlineAccountingMethodParams} from './UpdateQuickbooksOnlineAccountingMethodParams'; export type {default as UpdateXeroAccountingMethodParams} from './UpdateXeroAccountingMethodParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index cf78b7934e48..2d848ed76a2d 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -501,6 +501,11 @@ const WRITE_COMMANDS = { UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_ACCOUNT: 'UpdateSageIntacctNonreimbursableExpensesExportAccount', UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_VENDOR: 'UpdateSageIntacctNonreimbursableExpensesExportVendor', UPDATE_SAGE_INTACCT_TRAVEL_INVOICING_PAYABLE_ACCOUNT: 'UpdateSageIntacctTravelInvoicingPayableAccount', + CONNECT_POLICY_TO_RILLET: 'ConnectPolicyToRillet', + UPDATE_RILLET_SUBSIDIARY: 'UpdateRilletSubsidiary', + UPDATE_RILLET_ENABLE_NEW_CATEGORIES: 'UpdateRilletEnableNewCategories', + UPDATE_RILLET_SYNC_TAX_RATES: 'UpdateRilletSyncTaxRates', + UPDATE_RILLET_FIELD_MAPPING: 'UpdateRilletFieldMapping', SET_PROMO_CODE: 'User_SetPromoCode', REQUEST_TAX_EXEMPTION: 'RequestTaxExemption', EXPORT_SEARCH_ITEMS_TO_CSV: 'ExportSearchToCSV', @@ -1104,6 +1109,12 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_TAX_NON_BILLABLE]: Parameters.UpdateFinancialForceGenericTypeParams<'enabled', boolean>; [WRITE_COMMANDS.UPDATE_FINANCIAL_FORCE_EXPORT_FOREIGN_CURRENCY]: Parameters.UpdateFinancialForceGenericTypeParams<'enabled', boolean>; + [WRITE_COMMANDS.CONNECT_POLICY_TO_RILLET]: Parameters.ConnectPolicyToRilletParams; + [WRITE_COMMANDS.UPDATE_RILLET_SUBSIDIARY]: Parameters.UpdateRilletSubsidiaryParams; + [WRITE_COMMANDS.UPDATE_RILLET_ENABLE_NEW_CATEGORIES]: Parameters.UpdateRilletGenericTypeParams; + [WRITE_COMMANDS.UPDATE_RILLET_SYNC_TAX_RATES]: Parameters.UpdateRilletGenericTypeParams; + [WRITE_COMMANDS.UPDATE_RILLET_FIELD_MAPPING]: Parameters.UpdateRilletFieldMappingParams; + [WRITE_COMMANDS.UPGRADE_TO_CORPORATE]: Parameters.UpgradeToCorporateParams; [WRITE_COMMANDS.DOWNGRADE_TO_TEAM]: Parameters.DowngradeToTeamParams; [WRITE_COMMANDS.UPGRADE_SUBMIT]: Parameters.UpgradeSubmitParams; @@ -1305,6 +1316,7 @@ const READ_COMMANDS = { SYNC_POLICY_TO_GUSTO: 'SyncPolicyToGusto', SYNC_POLICY_TO_ZENEFITS: 'SyncPolicyToZenefits', SYNC_POLICY_TO_FINANCIAL_FORCE: 'SyncPolicyToFinancialForce', + SYNC_POLICY_TO_RILLET: 'SyncPolicyToRillet', CONNECT_POLICY_TO_FINANCIAL_FORCE: 'ConnectPolicyToFinancialForce', OPEN_REIMBURSEMENT_ACCOUNT_PAGE: 'OpenReimbursementAccountPage', OPEN_WORKSPACE_VIEW: 'OpenWorkspaceView', @@ -1408,6 +1420,7 @@ type ReadCommandParameters = { [READ_COMMANDS.SYNC_POLICY_TO_GUSTO]: Parameters.SyncPolicyToGustoParams; [READ_COMMANDS.SYNC_POLICY_TO_ZENEFITS]: Parameters.SyncPolicyToZenefitsParams; [READ_COMMANDS.SYNC_POLICY_TO_FINANCIAL_FORCE]: Parameters.SyncPolicyToFinancialForceParams; + [READ_COMMANDS.SYNC_POLICY_TO_RILLET]: Parameters.SyncPolicyToRilletParams; [READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: Parameters.OpenReimbursementAccountPageParams; [READ_COMMANDS.OPEN_WORKSPACE_VIEW]: Parameters.OpenWorkspaceViewParams; [READ_COMMANDS.GET_MAPBOX_ACCESS_TOKEN]: null; diff --git a/src/libs/AccountingUtils.ts b/src/libs/AccountingUtils.ts index ab9fb9063fc1..196b42251d91 100644 --- a/src/libs/AccountingUtils.ts +++ b/src/libs/AccountingUtils.ts @@ -9,6 +9,7 @@ const ROUTE_NAME_MAPPING = { [CONST.POLICY.CONNECTIONS.ROUTE.NETSUITE]: CONST.POLICY.CONNECTIONS.NAME.NETSUITE, [CONST.POLICY.CONNECTIONS.ROUTE.QBD]: CONST.POLICY.CONNECTIONS.NAME.QBD, [CONST.POLICY.CONNECTIONS.ROUTE.CERTINIA]: CONST.POLICY.CONNECTIONS.NAME.CERTINIA, + [CONST.POLICY.CONNECTIONS.ROUTE.RILLET]: CONST.POLICY.CONNECTIONS.NAME.RILLET, [CONST.POLICY.CONNECTIONS.ROUTE.GUSTO]: CONST.POLICY.CONNECTIONS.NAME.GUSTO, [CONST.POLICY.CONNECTIONS.ROUTE.ZENEFITS]: CONST.POLICY.CONNECTIONS.NAME.ZENEFITS, [CONST.POLICY.CONNECTIONS.ROUTE.MERGE_HR]: CONST.POLICY.CONNECTIONS.NAME.MERGE_HR, @@ -21,6 +22,7 @@ const NAME_ROUTE_MAPPING = { [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: CONST.POLICY.CONNECTIONS.ROUTE.NETSUITE, [CONST.POLICY.CONNECTIONS.NAME.QBD]: CONST.POLICY.CONNECTIONS.ROUTE.QBD, [CONST.POLICY.CONNECTIONS.NAME.CERTINIA]: CONST.POLICY.CONNECTIONS.ROUTE.CERTINIA, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: CONST.POLICY.CONNECTIONS.ROUTE.RILLET, [CONST.POLICY.CONNECTIONS.NAME.GUSTO]: CONST.POLICY.CONNECTIONS.ROUTE.GUSTO, [CONST.POLICY.CONNECTIONS.NAME.ZENEFITS]: CONST.POLICY.CONNECTIONS.ROUTE.ZENEFITS, [CONST.POLICY.CONNECTIONS.NAME.MERGE_HR]: CONST.POLICY.CONNECTIONS.ROUTE.MERGE_HR, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8a9479b116f7..05d9b9338d5d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -910,6 +910,10 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/certinia/export/CertiniaReportExportStatusPage').default, [SCREENS.WORKSPACE.ACCOUNTING.CERTINIA_COMPANY_SELECTOR]: () => require('../../../../pages/workspace/accounting/certinia/CertiniaCompanySelectorPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_SETUP]: () => require('../../../../pages/workspace/accounting/rillet/RilletSetupPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_EXISTING_CONNECTIONS]: () => require('../../../../pages/workspace/accounting/rillet/RilletExistingConnectionsPage').default, + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_SUBSIDIARY_SELECTOR]: () => require('../../../../pages/workspace/accounting/rillet/RilletSubsidiarySelector').default, + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_IMPORT]: () => require('../../../../pages/workspace/accounting/rillet/import/RilletImportPage').default, [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: () => require('../../../../pages/workspace/accounting/reconciliation/CardReconciliationPage').default, [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION_SAGE_INTACCT_AUTO_SYNC]: () => require('../../../../pages/workspace/accounting/reconciliation/CardReconciliationSageIntacctAutoSyncPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index cf0720357338..1fc75043d660 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -195,6 +195,10 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.CERTINIA_TAGS_MAPPING]: {path: ROUTES.POLICY_ACCOUNTING_CERTINIA_TAGS_MAPPING.route}, [SCREENS.WORKSPACE.ACCOUNTING.CERTINIA_REPORT_EXPORT_STATUS]: DYNAMIC_ROUTES.POLICY_ACCOUNTING_CERTINIA_REPORT_EXPORT_STATUS.path, [SCREENS.WORKSPACE.ACCOUNTING.CERTINIA_COMPANY_SELECTOR]: DYNAMIC_ROUTES.POLICY_ACCOUNTING_CERTINIA_COMPANY_SELECTOR.path, + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_SETUP]: {path: ROUTES.POLICY_ACCOUNTING_RILLET_SETUP.route}, + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_EXISTING_CONNECTIONS]: {path: ROUTES.POLICY_ACCOUNTING_RILLET_EXISTING_CONNECTIONS.route}, + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_SUBSIDIARY_SELECTOR]: {path: ROUTES.POLICY_ACCOUNTING_RILLET_SUBSIDIARY_SELECTOR.route}, + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_RILLET_IMPORT.route}, [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: {path: ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.route}, [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION_SAGE_INTACCT_AUTO_SYNC]: { path: ROUTES.POLICY_ACCOUNTING_CARD_RECONCILIATION_SAGE_INTACCT_AUTO_SYNC.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 502bf54ed2ad..5f13dc2069fc 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1192,6 +1192,18 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.CERTINIA_COMPANY_SELECTOR]: { policyID: string; }; + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_SETUP]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_EXISTING_CONNECTIONS]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_SUBSIDIARY_SELECTOR]: { + policyID: string; + }; + [SCREENS.WORKSPACE.ACCOUNTING.RILLET_IMPORT]: { + policyID: string; + }; [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: { policyID: string; connection: ValueOf; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 749b4dbb7215..773be9d41a38 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2795,6 +2795,10 @@ function getExportIntegrationActionFragments(translate: LocalizedTranslate, repo // The first three characters in a Salesforce ID is the expense type url = nonReimbursableUrls.at(0)?.substring(0, SALESFORCE_EXPENSES_URL_PREFIX.length + 3) ?? ''; break; + case CONST.EXPORT_LABELS.RILLET: + // TODO Test in R3 https://github.com/Expensify/App/issues/94848 + url = ''; + break; default: url = QBO_EXPENSES_URL; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1ec30f23752a..cf281ac9c067 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -12342,7 +12342,7 @@ function getSourceIDFromReportAction(reportAction: OnyxEntry): str function getIntegrationIcon( connectionName?: ConnectionName, - expensifyIcons?: Record<'XeroSquare' | 'QBOSquare' | 'NetSuiteSquare' | 'IntacctSquare' | 'QBDSquare' | 'CertiniaSquare' | 'GustoSquare', IconAsset> | undefined, + expensifyIcons?: Record<'XeroSquare' | 'QBOSquare' | 'NetSuiteSquare' | 'IntacctSquare' | 'QBDSquare' | 'CertiniaSquare' | 'RilletSquare' | 'GustoSquare', IconAsset> | undefined, ) { if (connectionName === CONST.POLICY.CONNECTIONS.NAME.XERO) { return expensifyIcons?.XeroSquare; @@ -12362,6 +12362,9 @@ function getIntegrationIcon( if (connectionName === CONST.POLICY.CONNECTIONS.NAME.CERTINIA) { return expensifyIcons?.CertiniaSquare; } + if (connectionName === CONST.POLICY.CONNECTIONS.NAME.RILLET) { + return expensifyIcons?.RilletSquare; + } if (connectionName === CONST.POLICY.CONNECTIONS.NAME.GUSTO) { return expensifyIcons?.GustoSquare; } diff --git a/src/libs/actions/connections/Rillet.ts b/src/libs/actions/connections/Rillet.ts new file mode 100644 index 000000000000..c667d0a5fd07 --- /dev/null +++ b/src/libs/actions/connections/Rillet.ts @@ -0,0 +1,316 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxUpdate} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; +import {write} from '@libs/API'; +import type {ConnectPolicyToRilletParams, UpdateRilletFieldMappingParams, UpdateRilletGenericTypeParams, UpdateRilletSubsidiaryParams} from '@libs/API/parameters'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Connections} from '@src/types/onyx/Policy'; + +function connectToRillet(policyID: string, apiKey: string) { + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`, + value: { + stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.RILLET_SYNC_CONNECTION, + connectionName: CONST.POLICY.CONNECTIONS.NAME.RILLET, + timestamp: new Date().toISOString(), + }, + }, + ]; + const parameters: ConnectPolicyToRilletParams = { + policyID, + apiKey, + }; + write(WRITE_COMMANDS.CONNECT_POLICY_TO_RILLET, parameters, {optimisticData}); +} + +function clearRilletErrorField(policyID: string, fieldName: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + connections: {[CONST.POLICY.CONNECTIONS.NAME.RILLET]: {config: {errorFields: {[fieldName]: null}}}}, + }); +} + +function prepareRilletOptimisticData( + policyID: string, + settingName: TSettingName, + settingValue: Partial, + oldSettingValue: Partial | null, +) { + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + rillet: { + config: { + [settingName]: settingValue ?? null, + pendingFields: { + [settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: { + [settingName]: null, + }, + }, + }, + }, + }, + }, + ]; + + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + rillet: { + config: { + pendingFields: { + [settingName]: null, + }, + }, + }, + }, + }, + }, + ]; + + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + rillet: { + config: { + [settingName]: oldSettingValue ?? null, + pendingFields: { + [settingName]: null, + }, + errorFields: { + [settingName]: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + }, + }, + }, + ]; + + return {optimisticData, successData, failureData}; +} + +function prepareRilletCodingOptimisticData( + policyID: string, + settingName: TSettingName, + settingValue: Partial, + oldSettingValue: Partial | null, +) { + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + rillet: { + config: { + coding: { + [settingName]: settingValue ?? null, + }, + pendingFields: { + [settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: { + [settingName]: null, + }, + }, + }, + }, + }, + }, + ]; + + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + rillet: { + config: { + pendingFields: { + [settingName]: null, + }, + }, + }, + }, + }, + }, + ]; + + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + rillet: { + config: { + coding: { + [settingName]: oldSettingValue ?? null, + }, + pendingFields: { + [settingName]: null, + }, + errorFields: { + [settingName]: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + }, + }, + }, + ]; + + return {optimisticData, successData, failureData}; +} + +function prepareRilletFieldMappingOptimisticData( + policyID: string, + fieldID: keyof Connections['rillet']['config']['coding']['fieldMappings'], + mapping: ValueOf, + oldMapping: ValueOf | null, +) { + const fieldOfflineFeedbackKey = `${CONST.RILLET_CONFIG.FIELD_MAPPING_PREFIX}${fieldID}`; + + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + rillet: { + config: { + coding: { + fieldMappings: { + [fieldID]: mapping, + }, + }, + pendingFields: { + [fieldOfflineFeedbackKey]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: { + [fieldOfflineFeedbackKey]: null, + }, + }, + }, + }, + }, + }, + ]; + + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + rillet: { + config: { + pendingFields: { + [fieldOfflineFeedbackKey]: null, + }, + }, + }, + }, + }, + }, + ]; + + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + connections: { + rillet: { + config: { + coding: { + fieldMappings: { + [fieldID]: oldMapping ?? null, + }, + }, + pendingFields: { + [fieldOfflineFeedbackKey]: null, + }, + errorFields: { + [fieldOfflineFeedbackKey]: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + }, + }, + }, + ]; + + return {optimisticData, successData, failureData}; +} + +function updateRilletSubsidiary(policyID: string, subsidiaryID: Connections['rillet']['config']['subsidiaryID'], oldSubsidiaryID?: Connections['rillet']['config']['subsidiaryID']) { + const onyxData = prepareRilletOptimisticData(policyID, CONST.RILLET_CONFIG.SUBSIDIARY_ID, subsidiaryID, oldSubsidiaryID ?? null); + const params: UpdateRilletSubsidiaryParams = { + policyID, + subsidiaryID, + }; + write(WRITE_COMMANDS.UPDATE_RILLET_SUBSIDIARY, params, onyxData); +} + +function updateRilletEnableNewCategories( + policyID: string, + enableNewCategories: Connections['rillet']['config']['enableNewCategories'], + oldEnableNewCategories?: Connections['rillet']['config']['enableNewCategories'], +) { + const onyxData = prepareRilletOptimisticData(policyID, CONST.RILLET_CONFIG.ENABLE_NEW_CATEGORIES, enableNewCategories, oldEnableNewCategories ?? null); + const parameters: UpdateRilletGenericTypeParams = { + policyID, + settingValue: JSON.stringify(enableNewCategories), + idempotencyKey: CONST.RILLET_CONFIG.ENABLE_NEW_CATEGORIES, + }; + write(WRITE_COMMANDS.UPDATE_RILLET_ENABLE_NEW_CATEGORIES, parameters, onyxData); +} + +function updateRilletSyncTaxRates( + policyID: string, + syncTaxRates: Connections['rillet']['config']['coding']['syncTaxRates'], + oldSyncTaxRates?: Connections['rillet']['config']['coding']['syncTaxRates'], +) { + const onyxData = prepareRilletCodingOptimisticData(policyID, CONST.RILLET_CONFIG.SYNC_TAX_RATES, syncTaxRates, oldSyncTaxRates ?? null); + const parameters: UpdateRilletGenericTypeParams = { + policyID, + settingValue: JSON.stringify(syncTaxRates), + idempotencyKey: CONST.RILLET_CONFIG.SYNC_TAX_RATES, + }; + write(WRITE_COMMANDS.UPDATE_RILLET_SYNC_TAX_RATES, parameters, onyxData); +} + +function updateRilletFieldMapping( + policyID: string, + fieldID: keyof Connections['rillet']['config']['coding']['fieldMappings'], + mapping: ValueOf, + oldMapping?: ValueOf, +) { + const onyxData = prepareRilletFieldMappingOptimisticData(policyID, fieldID, mapping, oldMapping ?? null); + const parameters: UpdateRilletFieldMappingParams = { + policyID, + fieldID, + mapping, + }; + write(WRITE_COMMANDS.UPDATE_RILLET_FIELD_MAPPING, parameters, onyxData); +} + +export {connectToRillet, clearRilletErrorField, updateRilletSubsidiary, updateRilletEnableNewCategories, updateRilletSyncTaxRates, updateRilletFieldMapping}; diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts index 56e1dda5b9b4..cf2982891692 100644 --- a/src/libs/actions/connections/index.ts +++ b/src/libs/actions/connections/index.ts @@ -148,6 +148,9 @@ function getSyncConnectionParameters(connectionName: PolicyConnectionName) { case CONST.POLICY.CONNECTIONS.NAME.CERTINIA: { return {readCommand: READ_COMMANDS.SYNC_POLICY_TO_FINANCIAL_FORCE, stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.FINANCIAL_FORCE_SYNC_TITLE}; } + case CONST.POLICY.CONNECTIONS.NAME.RILLET: { + return {readCommand: READ_COMMANDS.SYNC_POLICY_TO_RILLET, stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.RILLET_SYNC_CONNECTION}; + } default: return undefined; } @@ -357,6 +360,9 @@ function copyExistingPolicyConnection(connectedPolicyID: string, targetPolicyID: case CONST.POLICY.CONNECTIONS.NAME.CERTINIA: stageInProgress = CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.FINANCIAL_FORCE_SYNC_TITLE; break; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + stageInProgress = CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.RILLET_SYNC_CONNECTION; + break; default: stageInProgress = null; } diff --git a/src/pages/inbox/report/DynamicReportDetailsExportPage.tsx b/src/pages/inbox/report/DynamicReportDetailsExportPage.tsx index 7de56e559c07..2775b4d650c4 100644 --- a/src/pages/inbox/report/DynamicReportDetailsExportPage.tsx +++ b/src/pages/inbox/report/DynamicReportDetailsExportPage.tsx @@ -41,7 +41,7 @@ function DynamicReportDetailsExportPage({route}: DynamicReportDetailsExportPageP const {showConfirmModal} = useConfirmModal(); const styles = useThemeStyles(); const lazyIllustrations = useMemoizedLazyIllustrations(['LaptopWithSecondScreenAndHourglass']); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['XeroSquare', 'QBOSquare', 'NetSuiteSquare', 'IntacctSquare', 'QBDSquare', 'CertiniaSquare', 'GustoSquare']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['XeroSquare', 'QBOSquare', 'NetSuiteSquare', 'IntacctSquare', 'QBDSquare', 'CertiniaSquare', 'RilletSquare', 'GustoSquare']); const iconToDisplay = getIntegrationIcon(connectionName, expensifyIcons); const canBeExported = canBeExportedUtil(report); diff --git a/src/pages/workspace/accounting/AccountingContext/index.tsx b/src/pages/workspace/accounting/AccountingContext/index.tsx index 1b7764e7349b..cb3aa9d70cb1 100644 --- a/src/pages/workspace/accounting/AccountingContext/index.tsx +++ b/src/pages/workspace/accounting/AccountingContext/index.tsx @@ -29,10 +29,11 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider const [activeIntegration, setActiveIntegration] = useState(); const {translate} = useLocalize(); const policyID = policy?.id; - const accountingIcons = useMemoizedLazyExpensifyIcons(['IntacctSquare', 'QBOSquare', 'XeroSquare', 'NetSuiteSquare', 'QBDSquare', 'CertiniaSquare']); + const accountingIcons = useMemoizedLazyExpensifyIcons(['IntacctSquare', 'QBOSquare', 'XeroSquare', 'NetSuiteSquare', 'QBDSquare', 'CertiniaSquare', 'RilletSquare']); const hasReusablePoliciesConnectedToSageIntacct = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT, policyID); const hasReusablePoliciesConnectedToQBD = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.QBD, policyID); const hasReusablePoliciesConnectedToCertinia = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.CERTINIA, policyID); + const hasReusablePoliciesConnectedToRillet = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.RILLET, policyID); const startIntegrationFlow = useCallback( (newActiveIntegration: ActiveIntegration) => { @@ -48,7 +49,12 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider newActiveIntegration.name, policyID, translate, - {sageIntacct: hasReusablePoliciesConnectedToSageIntacct, qbd: hasReusablePoliciesConnectedToQBD, certinia: hasReusablePoliciesConnectedToCertinia}, + { + sageIntacct: hasReusablePoliciesConnectedToSageIntacct, + qbd: hasReusablePoliciesConnectedToQBD, + certinia: hasReusablePoliciesConnectedToCertinia, + rillet: hasReusablePoliciesConnectedToRillet, + }, undefined, undefined, newActiveIntegration.integrationToDisconnect, @@ -69,7 +75,16 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider key: Math.random(), }); }, - [policy, policyID, translate, hasReusablePoliciesConnectedToSageIntacct, hasReusablePoliciesConnectedToQBD, hasReusablePoliciesConnectedToCertinia, accountingIcons], + [ + policy, + policyID, + translate, + hasReusablePoliciesConnectedToSageIntacct, + hasReusablePoliciesConnectedToQBD, + hasReusablePoliciesConnectedToCertinia, + hasReusablePoliciesConnectedToRillet, + accountingIcons, + ], ); const closeConfirmationModal = () => { @@ -109,7 +124,12 @@ function AccountingContextProvider({children, policy}: AccountingContextProvider activeIntegration.name, policyID, translate, - {sageIntacct: hasReusablePoliciesConnectedToSageIntacct, qbd: hasReusablePoliciesConnectedToQBD, certinia: hasReusablePoliciesConnectedToCertinia}, + { + sageIntacct: hasReusablePoliciesConnectedToSageIntacct, + qbd: hasReusablePoliciesConnectedToQBD, + certinia: hasReusablePoliciesConnectedToCertinia, + rillet: hasReusablePoliciesConnectedToRillet, + }, policy, activeIntegration.key, undefined, diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index bf857385ae60..d98c059dfb40 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -80,6 +80,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const hasReusablePoliciesConnectedToSageIntacct = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT, policy?.id); const hasReusablePoliciesConnectedToQBD = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.QBD, policy?.id); const hasReusablePoliciesConnectedToCertinia = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.CERTINIA, policy?.id); + const hasReusablePoliciesConnectedToRillet = useHasReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.RILLET, policy?.id); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const theme = useTheme(); @@ -106,13 +107,23 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const allCardSettings = useExpensifyCardFeeds(policyID); const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'CircularArrowBackwards', 'ExpensifyCard', 'Gear', 'Key', 'NewWindow', 'Pencil', 'QuestionMark', 'Send', 'Sync', 'Trashcan']); - const accountingIcons = useMemoizedLazyExpensifyIcons(['IntacctSquare', 'QBOSquare', 'XeroSquare', 'NetSuiteSquare', 'QBDSquare', 'CertiniaSquare']); + const accountingIcons = useMemoizedLazyExpensifyIcons(['IntacctSquare', 'QBOSquare', 'XeroSquare', 'NetSuiteSquare', 'QBDSquare', 'CertiniaSquare', 'RilletSquare']); const illustrations = useMemoizedLazyIllustrations(['Accounting']); const canUseCertiniaIntegration = isBetaEnabled(CONST.BETAS.CERTINIA) || !!policy?.connections?.financialforce; + const canUseRilletIntegration = isBetaEnabled(CONST.BETAS.RILLET) || !!policy?.connections?.rillet; const accountingIntegrations = useMemo( - () => CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES.filter((name) => name !== CONST.POLICY.CONNECTIONS.NAME.CERTINIA || canUseCertiniaIntegration), - [canUseCertiniaIntegration], + () => + CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES.filter((name) => { + if (name === CONST.POLICY.CONNECTIONS.NAME.CERTINIA) { + return canUseCertiniaIntegration; + } + if (name === CONST.POLICY.CONNECTIONS.NAME.RILLET) { + return canUseRilletIntegration; + } + return true; + }), + [canUseCertiniaIntegration, canUseRilletIntegration], ); const syncingAccountingIntegration = accountingIntegrations.find((integration) => integration === connectionSyncProgress?.connectionName); const connectedIntegration = getConnectedIntegration(policy, accountingIntegrations) ?? syncingAccountingIntegration; @@ -262,6 +273,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { const integrationSpecificMenuItems = useMemo(() => { const sageIntacctEntityList = policy?.connections?.intacct?.data?.entities ?? []; const netSuiteSubsidiaryList = policy?.connections?.netsuite?.options?.data?.subsidiaryList ?? []; + const rilletSubsidiaryList = policy?.connections?.rillet?.data?.subsidiaries; const certiniaConfig = policy?.connections?.financialforce?.config; const certiniaCompanies = policy?.connections?.financialforce?.data?.companies ?? []; const certiniaCompanyID = certiniaConfig?.credentials?.companyID; @@ -354,6 +366,25 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { brickRoadIndicator: areSettingsInErrorFields([CONST.CERTINIA_CONFIG.COMPANY_ID], certiniaConfig?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, onPress: canWriteAccounting ? () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_CERTINIA_COMPANY_SELECTOR.getRoute(policyID)) : undefined, }; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return !rilletSubsidiaryList?.length + ? {} + : { + description: translate('workspace.rillet.subsidiary'), + iconRight: icons.ArrowRight, + title: rilletSubsidiaryList?.find((subsidiary) => subsidiary.id === policy?.connections?.rillet?.config?.subsidiaryID)?.tradeName ?? '', + wrapperStyle: [styles.sectionMenuItemTopDescription], + titleStyle: styles.fontWeightNormal, + shouldShowRightIcon: canWriteAccounting && rilletSubsidiaryList && rilletSubsidiaryList.length > 1, + shouldShowDescriptionOnTop: true, + interactive: canWriteAccounting, + pendingAction: policy?.connections?.rillet?.config.pendingFields?.subsidiaryID, + brickRoadIndicator: policy?.connections?.rillet?.config.errorFields?.subsidiaryID ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + onPress: + policyID && canWriteAccounting && rilletSubsidiaryList && rilletSubsidiaryList.length > 1 + ? () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_RILLET_SUBSIDIARY_SELECTOR.getRoute(policyID)) + : undefined, + }; default: return undefined; @@ -379,7 +410,12 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { integration, policyID, translate, - {sageIntacct: hasReusablePoliciesConnectedToSageIntacct, qbd: hasReusablePoliciesConnectedToQBD, certinia: hasReusablePoliciesConnectedToCertinia}, + { + sageIntacct: hasReusablePoliciesConnectedToSageIntacct, + qbd: hasReusablePoliciesConnectedToQBD, + certinia: hasReusablePoliciesConnectedToCertinia, + rillet: hasReusablePoliciesConnectedToRillet, + }, undefined, undefined, undefined, @@ -456,7 +492,12 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { connectedIntegration, policyID, translate, - {sageIntacct: hasReusablePoliciesConnectedToSageIntacct, qbd: hasReusablePoliciesConnectedToQBD, certinia: hasReusablePoliciesConnectedToCertinia}, + { + sageIntacct: hasReusablePoliciesConnectedToSageIntacct, + qbd: hasReusablePoliciesConnectedToQBD, + certinia: hasReusablePoliciesConnectedToCertinia, + rillet: hasReusablePoliciesConnectedToRillet, + }, policy, undefined, undefined, @@ -607,6 +648,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { datetimeToRelative, hasReusablePoliciesConnectedToSageIntacct, hasReusablePoliciesConnectedToCertinia, + hasReusablePoliciesConnectedToRillet, hasReusablePoliciesConnectedToQBD, canWriteAccounting, showReadOnlyModal, @@ -625,7 +667,12 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { integration, policyID, translate, - {sageIntacct: hasReusablePoliciesConnectedToSageIntacct, qbd: hasReusablePoliciesConnectedToQBD, certinia: hasReusablePoliciesConnectedToCertinia}, + { + sageIntacct: hasReusablePoliciesConnectedToSageIntacct, + qbd: hasReusablePoliciesConnectedToQBD, + certinia: hasReusablePoliciesConnectedToCertinia, + rillet: hasReusablePoliciesConnectedToRillet, + }, undefined, undefined, undefined, @@ -690,6 +737,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { translate, hasReusablePoliciesConnectedToSageIntacct, hasReusablePoliciesConnectedToCertinia, + hasReusablePoliciesConnectedToRillet, hasReusablePoliciesConnectedToQBD, styles.justifyContentCenter, styles.buttonOpacityDisabled, diff --git a/src/pages/workspace/accounting/rillet/RilletExistingConnectionsPage.tsx b/src/pages/workspace/accounting/rillet/RilletExistingConnectionsPage.tsx new file mode 100644 index 000000000000..09fdb64aa7c2 --- /dev/null +++ b/src/pages/workspace/accounting/rillet/RilletExistingConnectionsPage.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MenuItem from '@components/MenuItem'; +import MenuItemList from '@components/MenuItemList'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useReusablePoliciesConnectedTo from '@hooks/useReusablePoliciesConnectedTo'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {copyExistingPolicyConnection} from '@libs/actions/connections'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type RilletExistingConnectionsPageProps = PlatformStackScreenProps; + +function RilletExistingConnectionsPage({route}: RilletExistingConnectionsPageProps) { + const {translate, datetimeToRelative} = useLocalize(); + const styles = useThemeStyles(); + const icons = useMemoizedLazyExpensifyIcons(['LinkCopy']); + const policyID: string = route.params.policyID; + const {reusablePoliciesConnectedTo: reusablePoliciesConnectedToRillet} = useReusablePoliciesConnectedTo(CONST.POLICY.CONNECTIONS.NAME.RILLET, policyID); + + const menuItems = reusablePoliciesConnectedToRillet.map((policy) => { + const lastSuccessfulSyncDate = policy.connections?.rillet?.lastSync?.successfulDate; + const date = lastSuccessfulSyncDate ? datetimeToRelative(lastSuccessfulSyncDate) : undefined; + return { + title: policy.name, + key: policy.id, + avatarID: policy.id, + icon: policy.avatarURL ? policy.avatarURL : getDefaultWorkspaceAvatar(policy.name), + iconType: CONST.ICON_TYPE_WORKSPACE, + shouldShowRightIcon: true, + description: date ? translate('workspace.common.lastSyncDate', CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY.rillet, date) : translate('workspace.accounting.rillet'), + onPress: () => { + copyExistingPolicyConnection(policy.id, policyID, CONST.POLICY.CONNECTIONS.NAME.RILLET); + Navigation.dismissModal(); + }, + }; + }); + + return ( + + Navigation.goBack()} + /> + + {translate('workspace.common.existingConnectionsDescription', {connectionName: CONST.POLICY.CONNECTIONS.NAME.RILLET})} + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_RILLET_SETUP.getRoute(policyID))} + /> + {translate('workspace.common.existingConnections')} + + + + ); +} + +export default RilletExistingConnectionsPage; diff --git a/src/pages/workspace/accounting/rillet/RilletSetupPage.tsx b/src/pages/workspace/accounting/rillet/RilletSetupPage.tsx new file mode 100644 index 000000000000..97253d301c82 --- /dev/null +++ b/src/pages/workspace/accounting/rillet/RilletSetupPage.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import {View} from 'react-native'; +import ConnectionLayout from '@components/ConnectionLayout'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import RenderHTML from '@components/RenderHTML'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {isAuthenticationError} from '@libs/actions/connections'; +import {connectToRillet} from '@libs/actions/connections/Rillet'; +import {addErrorMessage} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/RilletCredentialsForm'; + +type RilletSetupPageProps = PlatformStackScreenProps; + +function RilletSetupPage({route}: RilletSetupPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const policyID: string = route.params.policyID; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const config = policy?.connections?.rillet?.config; + const shouldBeBlocked = !!config?.isConfigured && !isAuthenticationError(policy, CONST.POLICY.CONNECTIONS.NAME.RILLET); + + const confirmCredentials = (values: FormOnyxValues) => { + connectToRillet(policyID, values[INPUT_IDS.API_KEY]); + Navigation.dismissModal(); + }; + + const formItems = Object.values(INPUT_IDS); + const validate = (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + + for (const formItem of formItems) { + if (values[formItem]) { + continue; + } + addErrorMessage(errors, formItem, translate('common.error.fieldRequired')); + } + return errors; + }; + + return ( + + + {translate('workspace.rillet.enterCredentials')} + + + + + {formItems.map((formItem, index) => ( + + + + ))} + + + ); +} + +export default RilletSetupPage; diff --git a/src/pages/workspace/accounting/rillet/RilletSubsidiarySelector.tsx b/src/pages/workspace/accounting/rillet/RilletSubsidiarySelector.tsx new file mode 100644 index 000000000000..6e79455a4413 --- /dev/null +++ b/src/pages/workspace/accounting/rillet/RilletSubsidiarySelector.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import SelectionScreen from '@components/SelectionScreen'; +import type {SelectorType} from '@components/SelectionScreen'; +import Text from '@components/Text'; +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clearRilletErrorField, updateRilletSubsidiary} from '@libs/actions/connections/Rillet'; +import {getLatestErrorField} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {settingsPendingAction} from '@libs/PolicyUtils'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; + +function RilletSubsidiarySelector({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const subsidiaryList = policy?.connections?.rillet?.data?.subsidiaries; + const rilletConfig = policy?.connections?.rillet?.config; + const currentSubsidiaryID = rilletConfig?.subsidiaryID ?? CONST.DEFAULT_NUMBER_ID.toString(); + const policyID = policy?.id ?? CONST.DEFAULT_NUMBER_ID.toString(); + + const illustrations = useMemoizedLazyIllustrations(['Telescope']); + + const subsidiaryListSections = subsidiaryList + ? subsidiaryList.map((subsidiary) => ({ + text: subsidiary.tradeName, + keyForList: subsidiary.id, + isSelected: subsidiary.id === currentSubsidiaryID, + value: subsidiary.id, + })) + : []; + + const updateSubsidiary = ({keyForList, value}: SelectorType) => { + if (!keyForList || keyForList === currentSubsidiaryID) { + return; + } + + updateRilletSubsidiary(policyID, value, currentSubsidiaryID); + Navigation.goBack(); + }; + + const listEmptyContent = ( + + ); + + const listHeaderComponent = ( + + {translate('workspace.rillet.subsidiarySelectDescription')} + + ); + + return ( + Navigation.goBack()} + title="workspace.rillet.subsidiary" + listEmptyContent={listEmptyContent} + pendingAction={settingsPendingAction([CONST.RILLET_CONFIG.SUBSIDIARY_ID], rilletConfig?.pendingFields)} + errors={getLatestErrorField(rilletConfig ?? {}, CONST.RILLET_CONFIG.SUBSIDIARY_ID)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => clearRilletErrorField(policyID, CONST.RILLET_CONFIG.SUBSIDIARY_ID)} + /> + ); +} + +export default withPolicyConnections(RilletSubsidiarySelector); diff --git a/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx b/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx new file mode 100644 index 000000000000..09800673a869 --- /dev/null +++ b/src/pages/workspace/accounting/rillet/import/RilletImportPage.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import {View} from 'react-native'; +import ConnectionLayout from '@components/ConnectionLayout'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clearRilletErrorField, updateRilletEnableNewCategories, updateRilletFieldMapping, updateRilletSyncTaxRates} from '@libs/actions/connections/Rillet'; +import {getLatestErrorField} from '@libs/ErrorUtils'; +import {settingsPendingAction} from '@libs/PolicyUtils'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import CONST from '@src/CONST'; + +function RilletImportPage({policy}: WithPolicyConnectionsProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id; + const rilletConfig = policy?.connections?.rillet?.config; + const rilletData = policy?.connections?.rillet?.data; + + return ( + + + {translate('workspace.rillet.importDescription')} + + {}} + disabled + /> + policyID && updateRilletEnableNewCategories(policyID, !rilletConfig?.enableNewCategories, rilletConfig?.enableNewCategories)} + pendingAction={settingsPendingAction([CONST.RILLET_CONFIG.ENABLE_NEW_CATEGORIES], rilletConfig?.pendingFields)} + errors={getLatestErrorField(rilletConfig ?? {}, CONST.RILLET_CONFIG.ENABLE_NEW_CATEGORIES)} + onCloseError={() => policyID && clearRilletErrorField(policyID, CONST.RILLET_CONFIG.ENABLE_NEW_CATEGORIES)} + /> + + + {translate('workspace.rillet.dimensionsImport')} + + {rilletData?.fields.map((field) => ( + + policyID && + updateRilletFieldMapping( + policyID, + field.id, + rilletConfig?.coding.fieldMappings[field.id] === CONST.RILLET_MAPPING_VALUE.TAG ? CONST.RILLET_MAPPING_VALUE.NONE : CONST.RILLET_MAPPING_VALUE.TAG, + rilletConfig?.coding.fieldMappings[field.id], + ) + } + pendingAction={settingsPendingAction([`${CONST.RILLET_CONFIG.FIELD_MAPPING_PREFIX}${field.id}`], rilletConfig?.pendingFields)} + errors={getLatestErrorField(rilletConfig ?? {}, `${CONST.RILLET_CONFIG.FIELD_MAPPING_PREFIX}${field.id}`)} + onCloseError={() => policyID && clearRilletErrorField(policyID, `${CONST.RILLET_CONFIG.FIELD_MAPPING_PREFIX}${field.id}`)} + /> + ))} + {!!rilletData?.taxRates.length && ( + <> + + policyID && updateRilletSyncTaxRates(policyID, !rilletConfig?.coding.syncTaxRates, rilletConfig?.coding.syncTaxRates)} + pendingAction={settingsPendingAction([CONST.RILLET_CONFIG.SYNC_TAX_RATES], rilletConfig?.pendingFields)} + errors={getLatestErrorField(rilletConfig ?? {}, CONST.RILLET_CONFIG.SYNC_TAX_RATES)} + onCloseError={() => policyID && clearRilletErrorField(policyID, CONST.RILLET_CONFIG.SYNC_TAX_RATES)} + /> + + )} + + ); +} + +export default withPolicyConnections(RilletImportPage); diff --git a/src/pages/workspace/accounting/utils.tsx b/src/pages/workspace/accounting/utils.tsx index 387398e61b22..f41ac14a5745 100644 --- a/src/pages/workspace/accounting/utils.tsx +++ b/src/pages/workspace/accounting/utils.tsx @@ -4,6 +4,7 @@ import ConnectToCertiniaFlow from '@components/ConnectToCertiniaFlow'; import ConnectToNetSuiteFlow from '@components/ConnectToNetSuiteFlow'; import ConnectToQuickbooksDesktopFlow from '@components/ConnectToQuickbooksDesktopFlow'; import ConnectToQuickbooksOnlineFlow from '@components/ConnectToQuickbooksOnlineFlow'; +import ConnectToRilletFlow from '@components/ConnectToRilletFlow'; import ConnectToSageIntacctFlow from '@components/ConnectToSageIntacctFlow'; import ConnectToXeroFlow from '@components/ConnectToXeroFlow'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; @@ -45,13 +46,13 @@ function getAccountingIntegrationData( connectionName: PolicyConnectionName, policyID: string, translate: LocaleContextProps['translate'], - existingConnections: {sageIntacct: boolean; qbd: boolean; certinia: boolean}, + existingConnections: {sageIntacct: boolean; qbd: boolean; certinia: boolean; rillet: boolean}, policy?: Policy, key?: number, integrationToDisconnect?: ConnectionName, shouldDisconnectIntegrationBeforeConnecting?: boolean, canUseNetSuiteUSATax?: boolean, - expensifyIcons?: Record<'IntacctSquare' | 'QBOSquare' | 'XeroSquare' | 'NetSuiteSquare' | 'QBDSquare' | 'CertiniaSquare', IconAsset>, + expensifyIcons?: Record<'IntacctSquare' | 'QBOSquare' | 'XeroSquare' | 'NetSuiteSquare' | 'QBDSquare' | 'CertiniaSquare' | 'RilletSquare', IconAsset>, ): AccountingIntegration | undefined { const basePath = ROUTES.POLICY_ACCOUNTING.getRoute(policyID); const qboConfig = policy?.connections?.quickbooksOnline?.config; @@ -84,6 +85,15 @@ function getAccountingIntegrationData( } return ROUTES.POLICY_ACCOUNTING_CERTINIA_PREREQUISITES.getRoute(policyID); }; + const getBackToAfterWorkspaceUpgradeRouteForRillet = () => { + if (integrationToDisconnect) { + return ROUTES.POLICY_ACCOUNTING.getRoute(policyID, connectionName, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting); + } + if (existingConnections.rillet) { + return ROUTES.POLICY_ACCOUNTING_RILLET_EXISTING_CONNECTIONS.getRoute(policyID); + } + return ROUTES.POLICY_ACCOUNTING_RILLET_SETUP.getRoute(policyID); + }; switch (connectionName) { case CONST.POLICY.CONNECTIONS.NAME.QBO: @@ -380,6 +390,53 @@ function getAccountingIntegrationData( }, } as AccountingIntegration; } + case CONST.POLICY.CONNECTIONS.NAME.RILLET: { + return { + title: translate('workspace.accounting.rillet'), + icon: expensifyIcons?.RilletSquare, + setupConnectionFlow: ( + + ), + onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_RILLET_IMPORT.getRoute(policyID)), + subscribedImportSettings: [ + CONST.RILLET_CONFIG.ENABLE_NEW_CATEGORIES, + CONST.RILLET_CONFIG.SYNC_TAX_RATES, + ...(policy?.connections?.rillet?.data?.fields.map((field) => `${CONST.RILLET_CONFIG.FIELD_MAPPING_PREFIX}${field.id}`) ?? []), + ], + onExportPagePress: () => null, + subscribedExportSettings: [ + CONST.RILLET_CONFIG.EXPORTER, + CONST.RILLET_CONFIG.EXPORT_DATE, + CONST.RILLET_CONFIG.REIMBURSABLE, + CONST.RILLET_CONFIG.COMPANY_CARD, + CONST.RILLET_CONFIG.DEFAULT_VENDORID, + CONST.RILLET_CONFIG.CREDIT_CARD_ACCOUNTCODE, + CONST.RILLET_CONFIG.EXPORT_TO_MULTIPLE_ACCOUNTS, + CONST.RILLET_CONFIG.CARD_PROGRAM_ACCOUNTS, + ], + onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.ROUTE.RILLET)), + onAdvancedPagePress: () => null, + subscribedAdvancedSettings: [ + CONST.RILLET_CONFIG.ACCOUNTING_METHOD, + CONST.RILLET_CONFIG.AUTO_SYNC, + CONST.RILLET_CONFIG.SYNC_REIMBURSED_REPORTS, + CONST.RILLET_CONFIG.BILL_PAYMENT_ACCOUNT_CODE, + CONST.RILLET_CONFIG.SYNC_EXPENSIFY_CARD_SETTLEMENTS, + CONST.RILLET_CONFIG.SETTLEMENTS_BANK_ACCOUNT_ID, + CONST.RILLET_CONFIG.SYNC_TRAVEL_INVOICING_SETTLEMENTS, + CONST.RILLET_CONFIG.TRAVEL_INVOICING_SETTLEMENTS_BANK_ACCOUNT_ID, + ], + workspaceUpgradeNavigationDetails: { + integrationAlias: CONST.UPGRADE_FEATURE_INTRO_MAPPING.rillet.alias, + backToAfterWorkspaceUpgradeRoute: getBackToAfterWorkspaceUpgradeRouteForRillet(), + }, + pendingFields: policy?.connections?.rillet?.config?.pendingFields, + errorFields: policy?.connections?.rillet?.config?.errorFields, + }; + } default: return undefined; } diff --git a/src/pages/workspace/upgrade/UpgradeIntro.tsx b/src/pages/workspace/upgrade/UpgradeIntro.tsx index 1c6cce80aa8d..d1a6a0c8bbdb 100644 --- a/src/pages/workspace/upgrade/UpgradeIntro.tsx +++ b/src/pages/workspace/upgrade/UpgradeIntro.tsx @@ -91,7 +91,7 @@ function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizi 'Members', 'Approval', ]); - const illustrationIcons = useMemoizedLazyExpensifyIcons(['IntacctSquare', 'NetSuiteSquare', 'QBDSquare', 'CertiniaSquare', 'AdvancedApprovalsSquare', 'Unlock']); + const illustrationIcons = useMemoizedLazyExpensifyIcons(['IntacctSquare', 'NetSuiteSquare', 'QBDSquare', 'CertiniaSquare', 'RilletSquare', 'AdvancedApprovalsSquare', 'Unlock']); const imported = new Set([...Object.keys(illustrations), ...Object.keys(illustrationIcons)]); const missing = allIconNames.filter((n): n is string => !!n && !imported.has(n)); if (missing.length) { diff --git a/src/selectors/Policy.ts b/src/selectors/Policy.ts index 5210d4fd7ea8..649a4429c76e 100644 --- a/src/selectors/Policy.ts +++ b/src/selectors/Policy.ts @@ -12,7 +12,8 @@ type ReusablePolicyConnectionName = | typeof CONST.POLICY.CONNECTIONS.NAME.NETSUITE | typeof CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT | typeof CONST.POLICY.CONNECTIONS.NAME.QBD - | typeof CONST.POLICY.CONNECTIONS.NAME.CERTINIA; + | typeof CONST.POLICY.CONNECTIONS.NAME.CERTINIA + | typeof CONST.POLICY.CONNECTIONS.NAME.RILLET; const activePolicySelector = (policy: OnyxEntry) => (policy?.type !== CONST.POLICY.TYPE.PERSONAL ? policy : undefined); @@ -266,11 +267,15 @@ const adminPoliciesConnectedToNetSuiteSelector = (policies: OnyxCollection) => Object.values(policies ?? {}).filter((policy): policy is Policy => isAdminPolicyConnectedTo(policy, CONST.POLICY.CONNECTIONS.NAME.QBD)); +const adminPoliciesConnectedToRilletSelector = (policies: OnyxCollection) => + Object.values(policies ?? {}).filter((policy): policy is Policy => isAdminPolicyConnectedTo(policy, CONST.POLICY.CONNECTIONS.NAME.RILLET)); + const reusableConnectionAdminSelectors: Record) => Policy[]> = { [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: adminPoliciesConnectedToNetSuiteSelector, [CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]: adminPoliciesConnectedToSageIntacctSelector, [CONST.POLICY.CONNECTIONS.NAME.QBD]: adminPoliciesConnectedToQBDSelector, [CONST.POLICY.CONNECTIONS.NAME.CERTINIA]: adminPoliciesConnectedToCertiniaSelector, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: adminPoliciesConnectedToRilletSelector, }; function isReusablePolicyConnection(policy: Policy, connectionName: ReusablePolicyConnectionName, currentPolicyID?: string) { diff --git a/src/types/form/RilletCredentialsForm.ts b/src/types/form/RilletCredentialsForm.ts new file mode 100644 index 000000000000..4b110a7968d1 --- /dev/null +++ b/src/types/form/RilletCredentialsForm.ts @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + API_KEY: 'apiKey', +} as const; + +type InputID = ValueOf; + +type RilletCredentialsForm = Form< + InputID, + { + [INPUT_IDS.API_KEY]: string; + } +>; + +export type {RilletCredentialsForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 19e69e13ee4b..52c41c8dbdab 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -124,3 +124,4 @@ export type {EditAgentNameForm} from './EditAgentNameForm'; export type {EditAgentPromptForm} from './EditAgentPromptForm'; export type {AddAgentRuleForm} from './AddAgentRuleForm'; export type {EditAgentRuleForm} from './EditAgentRuleForm'; +export type {RilletCredentialsForm} from './RilletCredentialsForm'; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 7473b11e5d4c..116e90bee163 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1539,6 +1539,334 @@ type FinancialForceConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback< FinancialForceOfflineStateKeys >; +/** + * Supported subsidiary types in Rillet. + */ +type RilletSubsidiaryType = 'LEGAL_ENTITY'; + +/** + * A subsidiary (legal entity) configured in Rillet. + */ +type RilletSubsidiary = { + /** Unique identifier for the subsidiary. */ + id: string; + + /** Display or trade name of the subsidiary. */ + tradeName: string; + + /** Base accounting currency for the subsidiary (ISO currency code). */ + currency: string; + + /** Time zone used by the subsidiary. */ + timezone: string; + + /** Type of subsidiary. */ + type: RilletSubsidiaryType; +}; + +/** + * Supported account statuses in Rillet. + */ +type RilletAccountStatus = 'ACTIVE' | 'INACTIVE'; + +/** + * Supported chart of account categories in Rillet. + */ +type RilletAccountType = 'ASSET' | 'LIABILITY' | 'EQUITY' | 'EXPENSE' | 'INCOME'; + +/** + * A chart of accounts entry in Rillet. + */ +type RilletAccount = { + /** Unique identifier for the account. */ + id: string; + + /** Account code used in the chart of accounts. */ + code: string; + + /** Human-readable account name. */ + name: string; + + /** High-level account classification. */ + type: RilletAccountType; + + /** More specific account classification defined in Rillet. */ + subtype: string; + + /** Current status of the account. */ + status: RilletAccountStatus; + + /** Whether the account is used for intercompany transactions. */ + intercompany: boolean; + + /** Timestamp of the most recent update. */ + updatedAt: string; +}; + +/** + * A selectable value belonging to a custom field. + */ +type RilletFieldValue = { + /** Unique identifier for the field value. */ + id: string; + + /** Display name of the field value. */ + name: string; + + /** Whether the value has been deactivated. */ + deactivated: boolean; +}; + +/** + * A custom accounting field available in Rillet. + */ +type RilletField = { + /** Unique identifier for the field. */ + id: string; + + /** Display name of the field. */ + name: string; + + /** Available values that can be assigned to the field. */ + values: RilletFieldValue[]; + + /** Timestamp of the most recent update. */ + updatedAt: string; +}; + +/** + * A tax rate configured in Rillet. + */ +type RilletTaxRate = { + /** Unique identifier for the tax rate. */ + id: string; + + /** Tax code used for accounting purposes. */ + code: string; + + /** Country where the tax rate applies. */ + country: string; + + /** Description of the tax rate. */ + description: string; + + /** Tax percentage represented as a string value. */ + percentage: string; +}; + +/** + * A vendor or supplier configured in Rillet. + */ +type RilletVendor = { + /** Unique identifier for the vendor. */ + id: string; + + /** Vendor display name. */ + name: string; + + /** Vendor contact email address. */ + email?: string; + + /** Associated accounts payable account code. */ + accountCode?: string; + + /** Timestamp of the most recent update. */ + updatedAt: string; +}; + +/** + * Supported bank account statuses in Rillet. + */ +type RilletBankAccountStatus = 'ACTIVE' | 'INACTIVE'; + +/** + * A bank account configured in Rillet. + */ +type RilletBankAccount = { + /** Unique identifier for the bank account. */ + id: string; + + /** Display name of the bank account. */ + name: string; + + /** Currency of the bank account (ISO currency code). */ + currency: string; + + /** Name of the financial institution. */ + bankName: string; + + /** Identifier of the associated subsidiary, if applicable. */ + subsidiaryID?: string; + + /** Associated general ledger account code, if applicable. */ + accountCode?: string; + + /** Current status of the bank account. */ + status: RilletBankAccountStatus; +}; + +/** + * Cached reference data retrieved from Rillet and used for configuration. + */ +type RilletConnectionData = { + /** Collection of subsidiaries. */ + subsidiaries: RilletSubsidiary[]; + + /** Collection of accounts. */ + accounts: RilletAccount[]; + + /** Collection of custom fields. */ + fields: RilletField[]; + + /** Collection of tax rates. */ + taxRates: RilletTaxRate[]; + + /** Collection of vendors. */ + vendors: RilletVendor[]; + + /** Collection of bank accounts. */ + bankAccounts: RilletBankAccount[]; +}; + +/** + * Coding configuration used when exporting data to Rillet. + */ +type RilletCoding = { + /** + * Mapping of Rillet field IDs to their configured mapping behavior. + */ + fieldMappings: Record>; + + /** Whether tax rates should be synchronized from Rillet. */ + syncTaxRates: boolean; +}; + +/** Offline feedback key for field mapping */ +type RilletCodingFieldMappingsOfflineFeedbackKey = `${typeof CONST.RILLET_CONFIG.FIELD_MAPPING_PREFIX}${string}`; + +/** + * Offline feedback keys for `RilletCoding` + */ +type RilletCodingOfflineFeedbackKeys = keyof Omit | RilletCodingFieldMappingsOfflineFeedbackKey; + +/** + * Available dates that can be used as the export date. + */ +type RilletExportDate = 'LAST_EXPENSE' | 'REPORT_EXPORTED' | 'REPORT_SUBMITTED'; + +/** + * Export strategy for reimbursable expenses. + */ +type RilletExportReimbursable = 'VENDOR_BILL'; + +/** + * Export strategy for company card expenses. + */ +type RilletExportCompanyCard = 'CREDIT_CARD'; + +/** + * Export configuration for sending accounting data to Rillet. + */ +type RilletExport = { + /** Identifier of the export implementation to use. */ + exporter: string; + + /** Date source used when generating exported transactions. */ + exportDate: RilletExportDate; + + /** Export behavior for reimbursable expenses. */ + reimbursable: RilletExportReimbursable; + + /** Export behavior for company card expenses. */ + companyCard: RilletExportCompanyCard; + + /** Default vendor to associate with exported transactions. */ + defaultVendorID: string; + + /** Credit card liability account code. */ + creditCardAccountCode: string; + + /** + * Whether card transactions should be exported to multiple + * accounts based on card program mappings. + */ + exportToMultipleAccounts: boolean; + + /** + * Mapping of card program identifiers to account codes. + */ + cardProgramAccounts: Record; + + /** Accounting method used during export. */ + accountingMethod: string; +}; + +/** + * Automatic synchronization settings for Rillet. + */ +type RilletAutoSync = { + /** Whether automatic synchronization is enabled. */ + enabled: boolean; +}; + +/** + * Synchronization settings for importing and updating data in Rillet. + */ +type RilletSync = { + /** Whether reimbursed expense reports should be synchronized. */ + syncReimbursedReports: boolean; + + /** Account code used for bill payment transactions. */ + billPaymentAccountCode: string; + + /** Whether Expensify Card settlement transactions should be synchronized. */ + syncExpensifyCardSettlements: boolean; + + /** Bank account used for Expensify Card settlements. */ + settlementsBankAccountID: string; + + /** Whether travel invoicing settlement transactions should be synchronized. */ + syncTravelInvoicingSettlements: boolean; + + /** Bank account used for travel invoicing settlements. */ + travelInvoicingSettlementsBankAccountID: string; +}; + +/** + * Connection config for Rillet + */ +type RilletConnectionsConfig = OnyxCommon.OnyxValueWithOfflineFeedback< + { + /** The internalID of the selected subsidiary in Rillet */ + subsidiaryID: string; + + /** Whether the connection has been configured */ + isConfigured: boolean; + + /** Whether to enable a new Expense Category into Expensify */ + enableNewCategories: boolean; + + /** Coding settings */ + coding: RilletCoding; + + /** Export settings */ + export: RilletExport; + + /** Auto-sync settings */ + autoSync?: RilletAutoSync; + + /** Sync settings */ + sync: RilletSync; + + /** Collection of errors coming from BE */ + errors?: OnyxCommon.Errors; + + /** Collection of form field errors */ + errorFields?: OnyxCommon.ErrorFields; + }, + RilletCodingOfflineFeedbackKeys | keyof RilletExport | keyof RilletAutoSync | keyof RilletSync +>; + /** Gusto connection data */ type GustoConnectionData = Record; @@ -1741,6 +2069,9 @@ type Connections = { /** Certinia integration connection */ [CONST.POLICY.CONNECTIONS.NAME.CERTINIA]: Connection; + /** Rillet integration connection */ + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: Connection; + /** Gusto integration connection */ [CONST.POLICY.CONNECTIONS.NAME.GUSTO]: Connection;