Compare commits

...

34 Commits

Author SHA1 Message Date
Ron
50f30742a8 Merge pull request #309 from fleetbase/dev-v0.5.15
v0.5.15
2024-10-17 17:09:03 +08:00
Ronald A. Richardson
c7b1a876f5 v0.5.15 - Additional recovery actions added, added ability for admins to reset user password, patches to webhook handler and logging 2024-10-17 17:02:18 +08:00
Ron
892eaeeca0 Merge pull request #306 from fleetbase/dev-v0.5.14
v0.5.14
2024-10-15 17:53:05 +08:00
Ronald A. Richardson
32f4b69697 order list overlay revamp, ability to create customer with order in consumable api, few patches and fixes 2024-10-15 17:46:36 +08:00
Ron
6317c4b2e4 Merge pull request #304 from fleetbase/dev-v0.5.13
v0.1.3 - critical patches for driver creation flow on fleetops, added ability …
2024-10-10 19:46:55 +08:00
Ronald A. Richardson
e7c229ece5 critical patches for driver creation flow on fleetops, added ability for registry to handle tar/gz bundle uploads, added registry endpoint to upload bundles using auth token, few minor patches 2024-10-10 19:35:45 +08:00
Ron
983a3d22b5 Merge pull request #303 from fleetbase/dev-v0.5.12
v0.5.12 - bump version
2024-10-10 10:56:59 +08:00
Ronald A. Richardson
42105380ca bump version 2024-10-10 10:56:08 +08:00
Ron
8fd4a40016 Merge pull request #302 from fleetbase/dev-v0.5.12
v1.5.12 - critical hotfix to make sure session store is set for json resources
2024-10-10 10:55:42 +08:00
Ronald A. Richardson
331e98af20 critical hotfix to make sure session store is set for json resources 2024-10-10 10:52:37 +08:00
Ron
b1d226256a Merge pull request #301 from fleetbase/dev-v0.5.11
v0.5.11 - Added Manual Verify for Users [IAM], Added ability to Transfer Org Ow…
2024-10-10 00:50:23 +08:00
Ronald A. Richardson
f1ee8b0c99 Added Manual Verify for Users [IAM], Added ability to Transfer Org Ownership and Leave Orgs, Added recovery command, fixes and patches 2024-10-10 00:49:03 +08:00
Ron
23d5ecfdb8 Merge pull request #300 from fleetbase/dev-v0.5.10
patches and improvements
2024-10-08 21:42:07 +08:00
Ronald A. Richardson
2795a2f1be update app router.js 2024-10-08 21:41:07 +08:00
Ron
69afdee975 Merge pull request #299 from fleetbase/dev-v0.5.10
v0.5.10 - patches and improvements
2024-10-08 21:39:34 +08:00
Ronald A. Richardson
60845b9953 patches and improvements 2024-10-08 21:33:55 +08:00
Ron
f3997a1bb7 Merge pull request #298 from fleetbase/dev-v0.5.9
v0.5.9
2024-10-03 17:48:38 +08:00
Ronald A. Richardson
23a691e7e7 removed ember-leaflet patches, using perm fix and improved engine boot timeout and callbacks 2024-10-03 17:43:25 +08:00
Ronald A. Richardson
3dc562987a Hotfix release 2024-10-03 14:58:39 +08:00
Ron
e8ac2a3796 Merge pull request #295 from fleetbase/dev-v0.5.8
v0.5.8 - Feature: Customer Portal, Order Tracking Page, New Translations, Improved UX
2024-10-02 18:43:09 +08:00
Ronald A. Richardson
b78c59ad89 updated README roadmap 2024-10-02 18:38:17 +08:00
Ronald A. Richardson
bd71b1921b v0.5.8 ready 2024-10-02 18:34:40 +08:00
Ron
2596ccbded Merge pull request #290 from CassioDalla/console-pt-br-translation
Create pt-br.yaml File
2024-10-02 13:57:54 +08:00
Ronald A. Richardson
81159b7411 preparing for major release w/ customer portal extension and order tracking page, real time ETAs and order progress tracker info 2024-10-02 00:15:13 +08:00
Ronald A. Richardson
acc4cfba35 fix as-array utility 2024-09-06 12:18:11 +08:00
Ronald A. Richardson
ea47bdc09d little progress and bugfixes 2024-09-06 12:17:30 +08:00
Ronald A. Richardson
c60c460257 Feature: Customer Portal, improved Customer, Contact, and Vendor Management 2024-09-04 12:31:10 +08:00
Ron
dc00ac3892 Merge pull request #294 from fleetbase/dev-v0.5.7
upgrade core-api
2024-08-31 15:22:56 +07:00
Ronald A. Richardson
f18ec886a7 upgrade core-api 2024-08-31 16:21:45 +08:00
Ron
f039b61d79 Merge pull request #293 from fleetbase/dev-v0.5.7
hotfix for resource loading and relation loading for organizations
2024-08-31 15:01:36 +07:00
Ronald A. Richardson
248f70e31c hotfix for resource loading and relation loading for organizations 2024-08-31 15:51:30 +08:00
Ron
6bc76a1b33 Merge pull request #292 from fleetbase/dev-v0.5.6
v0.5.6 - hotfix only load organization with valid owners - no stale org
2024-08-30 17:18:07 +07:00
Ronald A. Richardson
30695b3ebe hotfix only load organization with valid owners - no stale org 2024-08-30 18:06:42 +08:00
CassioDalla
211a3a9808 Create pt-br.yaml File
Added Brazilian Portuguese translation file to the project. It would be nice if another translator reviewed it to make sure everything is correct.
2024-08-10 16:04:45 -03:00
107 changed files with 5029 additions and 3916 deletions

View File

@@ -132,11 +132,15 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Check for _GITHUB_AUTH_TOKEN and create .npmrc
- name: Create and Setup .npmrc
run: |
if [[ -n "${{ secrets._GITHUB_AUTH_TOKEN }}" ]]; then
echo "//npm.pkg.github.com/:_authToken=${{ secrets._GITHUB_AUTH_TOKEN }}" > .npmrc
fi
if [[ -n "${{ secrets.FLEETBASE_REGISTRY_TOKEN }}" ]]; then
echo "//registry.fleetbase.io/:_authToken=${{ secrets.FLEETBASE_REGISTRY_TOKEN }}" >> .npmrc
echo "@fleetbase:registry=https://registry.fleetbase.io" >> .npmrc
fi
working-directory: ./console
- name: Set Env Variables for QA

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ packages/flespi
packages/loconav
packages/internals
packages/projectargus-engine
packages/customer-portal
# wip
packages/solid
solid

View File

@@ -143,12 +143,11 @@ Fleetbase offers a few open sourced apps which are built on Fleetbase which can
</ul>
## 🛣️ Roadmap
1. **Customer Facing Views** ~ Extensions will be able to create public/customer facing views tracking and management from outside of the console UI.
2. **Inventory and Warehouse Management** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
3. **Accounting and Invoicing** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
4. **Binary Builds** ~ Run Fleetbase from a single binary.
6. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
7. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
1. **Inventory and Warehouse Management** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
2. **Accounting and Invoicing** ~ Ledger will be Fleetbases first official extension accounting and invoicing.
3. **Binary Builds** ~ Run Fleetbase from a single binary.
4. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
5. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
## 🪲 Bugs and 💡 Feature Requests

View File

@@ -9,10 +9,10 @@
"license": "AGPL-3.0-or-later",
"require": {
"php": "^8.0",
"fleetbase/core-api": "^1.5.3",
"fleetbase/fleetops-api": "^0.5.6",
"fleetbase/registry-bridge": "^0.0.13",
"fleetbase/storefront-api": "^0.3.14",
"fleetbase/core-api": "^1.5.15",
"fleetbase/fleetops-api": "^0.5.12",
"fleetbase/registry-bridge": "^0.0.17",
"fleetbase/storefront-api": "^0.3.16",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",

1155
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,4 +34,9 @@ return [
'api_key' => env('SENDGRID_API_KEY'),
],
'stripe' => [
'key' => env('STRIPE_KEY', env('STRIPE_API_KEY')),
'secret' => env('STRIPE_SECRET', env('STRIPE_API_SECRET')),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
];

View File

@@ -18,10 +18,6 @@ php artisan fleetbase:seed
# Create permissions, policies, and roles
php artisan fleetbase:create-permissions
# Assign admin and driver roles
php artisan fleetbase:assign-admin-roles
php artisan fleetops:assign-driver-roles
# Restart queue
php artisan queue:restart

View File

@@ -1,99 +0,0 @@
<div class="fleetbase-dashboard-grid flex items-center justify-between mb-4 mt-6 px-14">
<div class="left-section">
<h1 class="text-lg font-bold">{{this.dashboard.currentDashboard.name}}</h1>
</div>
<div class="fleetbase-dashboard-actions right-section ml-4 flex items-center">
<div class="fleetbase-model-select fleetbase-power-select ember-model-select h-10">
<DropdownButton
class="h-10"
@text={{if this.dashboard.currentDashboard.name this.dashboard.currentDashboard.name (t "component.dashboard.select-dashboard")}}
@textClass="text-sm mr-2"
@buttonClass="flex-row-reverse w-44 justify-between"
@icon="caret-down"
@iconClass="mr-0i"
@size="sm"
@iconPrefix="fas"
@triggerClass="hidden md:flex"
as |dd|
>
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
<div class="p-1">
{{#each this.dashboard.dashboards as |dashboard|}}
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.selectDashboard dashboard)}}>
<div class="flex-1 flex flex-row items-center">
<div class="w-6">
<FaIcon @icon="desktop" />
</div>
<span>{{dashboard.name}}</span>
</div>
<div>
{{#if (eq this.dashboard.currentDashboard.id dashboard.id)}}
<FaIcon @icon="check" class="text-green-500" />
{{/if}}
</div>
</a>
{{/each}}
</div>
</div>
</DropdownButton>
</div>
<div class="ml-2 relative h-10">
<DropdownButton class="h-10" @icon="ellipsis-h" @size="sm" @iconPrefix="fas" @triggerClass="hidden md:flex" as |dd|>
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
<div class="p-1">
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.createDashboard)}}>
<div class="w-6">
<FaIcon @icon="add" />
</div>
<span>{{t "component.dashboard.create-new-dashboard"}}</span>
</a>
{{#unless (eq this.dashboard.currentDashboard.user_uuid "system")}}
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onChangeEdit true)}}>
<div class="w-6">
<FaIcon @icon="edit" />
</div>
<span>{{t "component.dashboard.edit-layout"}}</span>
</a>
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.onAddingWidget true)}}>
<div class="w-6">
<FaIcon @icon="add" />
</div>
<span>{{t "component.dashboard.add-widgets"}}</span>
</a>
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.deleteDashboard this.dashboard.currentDashboard)}}>
<div class="w-6">
<FaIcon @icon="trash" />
</div>
<span>{{t "component.dashboard.delete-dashboard"}}</span>
</a>
{{/unless}}
</div>
</div>
</DropdownButton>
</div>
{{#if this.dashboard.isEditingDashboard}}
<div class="ml-2 h-10">
<Button @type="magic" @icon="save" @helpText={{t "component.dashboard.save-dashboard"}} @onClick={{fn this.onChangeEdit false}} class="h-10" />
</div>
{{/if}}
</div>
</div>
<div class="px-10">
<Dashboard::Create @isEdit={{this.dashboard.isEditingDashboard}} @isAddingWidget={{this.dashboard.isAddingWidget}} @dashboard={{this.dashboard.currentDashboard}} />
{{#if this.dashboard.isAddingWidget}}
<EmberWormhole @to="console-home-wormhole">
<Dashboard::WidgetPanel
@isOpen={{this.dashboard.isAddingWidget}}
@onLoad={{this.setWidgetSelectorPanelContext}}
@dashboard={{this.dashboard.currentDashboard}}
@onClose={{fn this.onAddingWidget false}}
/>
</EmberWormhole>
{{/if}}
</div>

View File

@@ -1,140 +0,0 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
/**
* DashboardComponent for managing dashboards in an Ember application.
* This component handles actions such as selecting, creating, deleting dashboards,
* and managing widget selectors and dashboard editing states.
*
* @extends Component
*/
export default class DashboardComponent extends Component {
/**
* Ember Data store service.
* @type {Service}
*/
@service store;
/**
* Internationalization service for managing translations.
* @type {Service}
*/
@service intl;
/**
* Notifications service for displaying alerts or confirmations.
* @type {Service}
*/
@service notifications;
/**
* Modals manager service for handling modal dialogs.
* @type {Service}
*/
@service modalsManager;
/**
* Fetch service for handling HTTP requests.
* @type {Service}
*/
@service fetch;
/**
* Dashboard service for business logic related to dashboards.
* @type {Service}
*/
@service dashboard;
/**
* Creates an instance of DashboardComponent.
* @memberof DashboardComponent
*/
constructor() {
super(...arguments);
this.dashboard.loadDashboards.perform();
}
/**
* Action to select a dashboard.
* @param {Object} dashboard - The dashboard to be selected.
*/
@action selectDashboard(dashboard) {
this.dashboard.selectDashboard.perform(dashboard);
}
/**
* Sets the context for the widget selector panel.
* @param {Object} widgetSelectorContext - The context object for the widget selector.
*/
@action setWidgetSelectorPanelContext(widgetSelectorContext) {
this.widgetSelectorContext = widgetSelectorContext;
}
/**
* Creates a new dashboard.
* @param {Object} dashboard - The dashboard to be created.
* @param {Object} [options={}] - Optional parameters for dashboard creation.
*/
@action createDashboard(dashboard, options = {}) {
this.modalsManager.show('modals/create-dashboard', {
title: this.intl.t('component.dashboard.create-a-new-dashboard'),
acceptButtonText: this.intl.t('component.dashboard.confirm-create-dashboard'),
confirm: async (modal, done) => {
modal.startLoading();
// Get the name from the modal options
const { name } = modal.getOptions();
await this.dashboard.createDashboard.perform(name);
done();
},
...options,
});
}
/**
* Deletes a dashboard.
* @param {Object} dashboard - The dashboard to be deleted.
* @param {Object} [options={}] - Optional parameters for dashboard deletion.
*/
@action deleteDashboard(dashboard, options = {}) {
if (this.dashboard.dashboards?.length === 1) {
return this.notifications.error(this.intl.t('component.dashboard.you-cannot-delete-this-dashboard'));
}
this.modalsManager.confirm({
title: this.intl.t('component.dashboard.are-you-sure-you-want-delete-dashboard', { dashboardName: dashboard.name }),
confirm: async (modal, done) => {
modal.startLoading();
await this.dashboard.deleteDashboard.perform(dashboard);
done();
},
...options,
});
}
/**
* Action to handle the addition of a widget.
* @param {boolean} [state=true] - The state to set for adding a widget.
*/
@action onAddingWidget(state = true) {
this.dashboard.onAddingWidget(state);
}
/**
* Sets the current dashboard.
* @param {Object} dashboard - The dashboard to be set as current.
*/
@action setCurrentDashboard(dashboard) {
this.dashboard.setCurrentDashboard.perform(dashboard);
}
/**
* Changes the editing state of the dashboard.
* @param {boolean} [state=true] - The state to set for editing the dashboard.
*/
@action onChangeEdit(state = true) {
this.dashboard.onChangeEdit(state);
}
}

View File

@@ -1,16 +0,0 @@
<div class="fleetbase-dashboard-grid" ...attributes>
<GridStack @options={{this.gridOptions}} @onChange={{this.onChangeGrid}}>
{{#each @dashboard.widgets as |widget|}}
{{#if (component-resolvable widget.component)}}
<GridStackItem id={{widget.id}} @options={{spread-widget-options (hash id=widget.id options=widget.grid_options)}} class="relative">
{{component widget.component options=widget.options}}
{{#if @isEdit}}
<div class="absolute top-2 right-2">
<Button @type="default" @icon="trash" @helpText={{"Remove widget from the dashboard"}} @onClick={{fn this.removeWidget widget}} />
</div>
{{/if}}
</GridStackItem>
{{/if}}
{{/each}}
</GridStack>
</div>

View File

@@ -1,99 +0,0 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action, computed } from '@ember/object';
import { inject as service } from '@ember/service';
/**
* Component responsible for creating and managing the dashboard layout.
* Provides functionalities such as toggling widget float, changing grid layout, and removing widgets.
*
* @extends Component
*/
export default class DashboardCreateComponent extends Component {
/**
* Notifications service for displaying alerts or errors.
* @type {Service}
*/
@service notifications;
/**
* Tracked array to keep track of widgets that have been updated.
* @type {Array}
*/
@tracked updatedWidgets = [];
/**
* Action to toggle the floating state of widgets on the grid.
*/
@action toggleFloat() {
this.shouldFloat = !this.shouldFloat;
}
/**
* Handles changes to the grid layout, such as repositioning or resizing widgets.
* Iterates over each widget event detail and updates the corresponding widget's properties if necessary.
*
* @param {Event} event - Event containing details about the grid change.
* @action
*/
@action onChangeGrid(event) {
const { dashboard } = this.args;
event.detail.forEach((currentWidgetEvent) => {
const alreadyUpdated = this.updatedWidgets.find((item) => item.id === currentWidgetEvent.id);
if (alreadyUpdated || !this.dashboard) {
return;
}
const changedWidget = dashboard.widgets.find((widget) => widget.id === currentWidgetEvent.id);
if (!changedWidget) {
return;
}
const { x, y, w, h } = currentWidgetEvent;
const response = changedWidget.updateProperties({
grid_options: { x, y, w, h },
});
if (response) {
this.updatedWidgets.push(changedWidget);
}
});
}
/**
* Removes a specified widget from the dashboard.
* Performs a removal operation on the dashboard and handles any errors that occur during the process.
*
* @param {Object} widget - The widget object to be removed.
* @action
*/
@action removeWidget(widget) {
const { dashboard } = this.args;
if (dashboard) {
dashboard.removeWidget(widget.id).catch((error) => {
this.notifications.serverError(error);
});
}
}
/**
* Computed property that returns grid options based on the current edit state.
* Configures grid behavior such as floating, animation, and drag and resize capabilities.
*
* @computed
* @returns {Object} An object containing grid configuration options.
*/
@computed('args.isEdit') get gridOptions() {
return {
float: true,
animate: true,
acceptWidgets: true,
alwaysShowResizeHandle: this.args.isEdit,
disableDrag: !this.args.isEdit,
disableResize: !this.args.isEdit,
resizable: { handles: 'all' },
cellHeight: 30,
};
}
}

View File

@@ -0,0 +1,12 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container pt-0i text-gray-900 dark:text-white">
<InputGroup @name="Organization name" @value={{@options.organization.name}} />
<InputGroup @name="Organization description" @value={{@options.organization.description}} />
<InputGroup @name="Organization phone number">
<PhoneInput @value={{@options.organization.phone}} @onInput={{fn (mut @options.organization.phone)}} class="form-input w-full" />
</InputGroup>
<InputGroup @name="Organization currency">
<CurrencySelect @value={{@options.organization.currency}} @onSelect={{fn (mut @options.organization.currency)}} @triggerClass="w-full form-select" />
</InputGroup>
</div>
</Modal::Default>

View File

@@ -0,0 +1,3 @@
import Component from '@glimmer/component';
export default class ModalsEditOrganizationComponent extends Component {}

View File

@@ -0,0 +1,43 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container pt-0i text-gray-900 dark:text-white">
{{#if @options.isOwner}}
{{#if @options.hasOtherMembers}}
<p>
<div class="text-base mb-2">
As the owner of
<strong>{{@options.organization.name}}</strong>, leaving the organization requires you to nominate a new owner.
</div>
<div>Please select a member from the dropdown below to transfer ownership before you can proceed.</div>
</p>
<InputGroup @name="Select a New Owner" @wrapperClass="mt-2 mb-0i">
<Select
@options={{@options.organization.users}}
@value={{@options.newOwnerId}}
@onSelect={{@options.selectNewOwner}}
@optionLabel="name"
@optionValue="id"
@placeholder="Select a member"
/>
</InputGroup>
{{else if @options.willBeDeleted}}
<p>
<div class="text-base mb-2">
You are the sole owner of
<strong>{{@options.organization.name}}</strong>.
</div>
<div>By leaving, the organization will be permanently deleted along with all its data.</div>
<div>Are you sure you want to proceed?</div>
</p>
<p class="mt-3"><em>This action cannot be undone.</em></p>
{{/if}}
{{else}}
<p>
<div class="text-base mb-2">
Are you sure you want to leave the organization
<strong>{{@options.organization.name}}</strong>?
</div>
<div>You will no longer have access to its resources and settings.</div>
</p>
{{/if}}
</div>
</Modal::Default>

View File

@@ -0,0 +1,3 @@
import Component from '@glimmer/component';
export default class ModalsLeaveOrganizationComponent extends Component {}

View File

@@ -4,25 +4,8 @@ import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';
export default class AuthForgotPasswordController extends Controller {
/**
* Inject the `fetch` service
*
* @memberof AuthForgotPasswordController
*/
@service fetch;
/**
* Inject the `notifications` service
*
* @memberof AuthForgotPasswordController
*/
@service notifications;
/**
* Inject the `intl` service
*
* @memberof AuthForgotPasswordController
*/
@service intl;
/**

View File

@@ -5,53 +5,12 @@ import { action } from '@ember/object';
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
export default class AuthLoginController extends Controller {
/**
* Inject the `forgotPassword` controller
*
* @var {Controller}
*/
@controller('auth.forgot-password') forgotPasswordController;
/**
* Inject the `notifications` service
*
* @var {Service}
*/
@service notifications;
/**
* Inject the `urlSearchParams` service
*
* @var {Service}
*/
@service urlSearchParams;
/**
* Inject the `session` service
*
* @var {Service}
*/
@service session;
/**
* Inject the `router` service
*
* @var {Service}
*/
@service router;
/**
* Inject the `intl` service
*
* @var {Service}
*/
@service intl;
/**
* Inject the `fetch` service
*
* @var {Service}
*/
@service fetch;
/**
@@ -110,8 +69,20 @@ export default class AuthLoginController extends Controller {
*/
@tracked failedAttempts = 0;
/**
* Authentication token.
*
* @memberof AuthLoginController
*/
@tracked token;
/**
* Action to login user.
*
* @param {Event} event
* @return {void}
* @memberof AuthLoginController
*/
@action async login(event) {
// firefox patch
event.preventDefault();

View File

@@ -155,13 +155,13 @@ export default class ConsoleController extends Controller {
*
* @void
*/
@action invalidateSession(noop, event) {
@action async invalidateSession(noop, event) {
event.preventDefault();
this.session.invalidateWithLoader();
await this.session.invalidateWithLoader();
}
/**
* Action to invalidate and log user out
* Action to create or join an organization.
*
* @void
*/

View File

@@ -12,32 +12,9 @@ import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
* @extends Controller
*/
export default class ConsoleAccountAuthController extends Controller {
/**
* Service for handling data fetching.
*
* @type {fetch}
*/
@service fetch;
/**
* Service for displaying notifications.
*
* @type {notifications}
*/
@service notifications;
/**
* Service for managing application routing.
*
* @type {router}
*/
@service router;
/**
* Service for managing modals.
*
* @type {router}
*/
@service modalsManager;
/**
@@ -187,14 +164,12 @@ export default class ConsoleAccountAuthController extends Controller {
* @param {Object} twoFaSettings - User-specific two-factor authentication settings.
*/
@task *saveUserTwoFaSettings(twoFaSettings = {}) {
yield this.fetch
.post('users/two-fa', { twoFaSettings })
.then(() => {
this.notifications.success('2FA Settings saved successfully.');
})
.catch((error) => {
this.notifications.serverError(error);
});
try {
yield this.fetch.post('users/two-fa', { twoFaSettings });
this.notifications.success('2FA Settings saved successfully.');
} catch (error) {
this.notifications.serverError(error);
}
}
/**
@@ -203,13 +178,17 @@ export default class ConsoleAccountAuthController extends Controller {
* @method loadUserTwoFaSettings
*/
@task *loadUserTwoFaSettings() {
const twoFaSettings = yield this.fetch.get('users/two-fa');
try {
const twoFaSettings = yield this.fetch.get('users/two-fa');
if (twoFaSettings) {
this.isUserTwoFaEnabled = twoFaSettings.enabled;
this.twoFaSettings = twoFaSettings;
}
if (twoFaSettings) {
this.isUserTwoFaEnabled = twoFaSettings.enabled;
this.twoFaSettings = twoFaSettings;
return twoFaSettings;
} catch (error) {
this.notifications.serverError(error);
}
return twoFaSettings;
}
/**
@@ -218,12 +197,16 @@ export default class ConsoleAccountAuthController extends Controller {
* @method loadSystemTwoFaConfig
*/
@task *loadSystemTwoFaConfig() {
const twoFaConfig = yield this.fetch.get('two-fa/config');
try {
const twoFaConfig = yield this.fetch.get('two-fa/config');
if (twoFaConfig) {
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
this.twoFaConfig = twoFaConfig;
}
if (twoFaConfig) {
this.isSystemTwoFaEnabled = twoFaConfig.enabled;
this.twoFaConfig = twoFaConfig;
return twoFaConfig;
} catch (error) {
this.notifications.serverError(error);
}
return twoFaConfig;
}
}

View File

@@ -0,0 +1,203 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
import { htmlSafe } from '@ember/template';
export default class ConsoleAccountOrganizationsController extends Controller {
@service currentUser;
@service modalsManager;
@service crud;
@service notifications;
@service intl;
@service fetch;
@service router;
@action async leaveOrganization(organization) {
const isOwner = this.currentUser.id === organization.owner_uuid;
const hasOtherMembers = organization.users_count > 1;
const willBeDeleted = isOwner && organization.users_count === 1;
if (this.model.length === 1) {
return this.notifications.warning('Unable to leave your only organization.');
}
if (hasOtherMembers) {
organization.loadUsers({ exclude: [this.currentUser.id] });
}
this.modalsManager.show('modals/leave-organization', {
title: isOwner ? (willBeDeleted ? 'Delete Organization' : 'Transfer Ownership and Leave') : 'Leave Organization',
acceptButtonText: isOwner ? (willBeDeleted ? 'Delete Organization' : 'Transfer Ownership and Leave') : 'Leave Organization',
acceptButtonScheme: 'danger',
acceptButtonIcon: isOwner ? (willBeDeleted ? 'trash' : 'person-walking-arrow-right') : 'person-walking-arrow-right',
acceptButtonDisabled: isOwner && hasOtherMembers,
isOwner,
hasOtherMembers,
willBeDeleted,
organization,
newOwnerId: null,
selectNewOwner: (newOwnerId) => {
this.modalsManager.setOption('newOwnerId', newOwnerId);
this.modalsManager.setOption('acceptButtonDisabled', false);
},
confirm: async (modal) => {
modal.startLoading();
if (isOwner) {
if (hasOtherMembers) {
const newOwnerId = this.modalsManager.getOption('newOwnerId');
try {
await organization.transferOwnership(newOwnerId, { leave: true });
} catch (error) {
this.notifications.serverError(error);
}
return this.router.refresh();
}
if (willBeDeleted) {
try {
await organization.destroyRecord();
} catch (error) {
this.notifications.serverError(error);
}
return this.router.refresh();
}
}
try {
await organization.leave();
} catch (error) {
this.notifications.serverError(error);
}
return this.router.refresh();
},
});
}
@action switchOrganization(organization) {
this.modalsManager.confirm({
title: this.intl.t('console.switch-organization.modal-title', { organizationName: organization.name }),
body: this.intl.t('console.switch-organization.modal-body'),
acceptButtonText: this.intl.t('console.switch-organization.modal-accept-button-text'),
acceptButtonScheme: 'primary',
confirm: async (modal) => {
modal.startLoading();
try {
await this.fetch.post('auth/switch-organization', { next: organization.uuid });
this.fetch.flushRequestCache('auth/organizations');
this.notifications.success(this.intl.t('console.switch-organization.success-notification'));
return later(
this,
() => {
window.location.reload();
},
900
);
} catch (error) {
modal.stopLoading();
return this.notifications.serverError(error);
}
},
});
}
@action deleteOrganization(organization) {
const isOwner = this.currentUser.id === organization.owner_uuid;
if (this.model.length === 1) {
return this.notifications.warning('Unable to delete your only organization.');
}
if (!isOwner) {
return this.notifications.warning('You do not have rights to delete this organization.');
}
this.crud.delete(organization, {
title: `Are you sure you want to delete the organization ${organization.name}?`,
body: htmlSafe(
`This action will permanently remove all data, including orders, members, and settings associated with the organization. <br /><br /><strong>This action cannot be undone.</strong>`
),
acceptButtonText: 'Delete Organization',
acceptButtonScheme: 'danger',
acceptButtonIcon: 'trash',
confirm: async (modal) => {
modal.startLoading();
try {
await organization.destroyRecord();
return this.router.refresh();
} catch (error) {
this.notifications.serverError(error);
}
},
});
}
@action editOrganization(organization) {
this.modalsManager.show('modals/edit-organization', {
title: 'Edit Organization',
acceptButtonText: 'Save Changes',
acceptButtonIcon: 'save',
isOwner: this.currentUser.id === organization.owner_uuid,
organization,
confirm: async (modal) => {
modal.startLoading();
try {
await organization.save();
return this.router.refresh();
} catch (error) {
this.notifications.serverError(error);
}
},
});
}
@action createOrganization() {
const currency = this.currentUser.currency;
const country = this.currentUser.country;
this.modalsManager.show('modals/edit-organization', {
title: 'Create Organization',
acceptButtonText: this.intl.t('common.confirm'),
acceptButtonIcon: 'check',
acceptButtonIconPrefix: 'fas',
organization: {
name: null,
decription: null,
phone: null,
currency,
country,
timezone: null,
},
confirm: async (modal) => {
modal.startLoading();
const organization = modal.getOption('organization');
const { name, description, phone, currency, country, timezone } = organization;
try {
await this.fetch.post('auth/create-organization', {
name,
description,
phone,
currency,
country,
timezone,
});
this.fetch.flushRequestCache('auth/organizations');
this.notifications.success(this.intl.t('console.create-or-join-organization.create-success-notification'));
return this.router.refresh();
} catch (error) {
modal.stopLoading();
return this.notifications.serverError(error);
}
},
});
}
}

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleAccountVirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleAdminVirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -1,3 +1,26 @@
import Controller from '@ember/controller';
export default class ConsoleHomeController extends Controller {}
export default class ConsoleHomeController extends Controller {
rows = [
{
name: 'Jason',
age: 24,
vehicle: 'Honda',
},
];
columns = [
{
label: 'Name',
valuePath: 'name',
},
{
label: 'Age',
valuePath: 'age',
},
{
label: 'Vehicle',
valuePath: 'vehicle',
},
];
}

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleSettingsVirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class ConsoleVirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -1,7 +0,0 @@
import { helper } from '@ember/component/helper';
export default helper(function spreadWidgetOptions([params]) {
const { id, options } = params;
const gridOptions = { id, ...options };
return gridOptions;
});

View File

@@ -1,8 +1,8 @@
export function initialize(owner) {
const universe = owner.lookup('service:universe');
export function initialize(application) {
const universe = application.lookup('service:universe');
if (universe) {
universe.createRegistry('@fleetbase/console');
universe.bootEngines(owner);
universe.createRegistries(['@fleetbase/console', 'auth:login']);
universe.bootEngines(application);
}
}

View File

@@ -0,0 +1,18 @@
export function initialize(application) {
const leafletService = application.lookup('service:leaflet');
if (leafletService) {
leafletService.load({
onReady: function (L) {
// This will prevent the awkward scroll bug produced by Chrome browsers
// https://github.com/Leaflet/Leaflet/issues/4125#issuecomment-356289643
L.Control.include({
_refocusOnMap: L.Util.falseFn,
});
},
});
}
}
export default {
initialize,
};

View File

@@ -1,5 +1,6 @@
import Model, { attr, belongsTo } from '@ember-data/model';
import { computed } from '@ember/object';
import { getOwner } from '@ember/application';
import { format, formatDistanceToNow } from 'date-fns';
import autoSerialize from '../utils/auto-serialize';
@@ -34,6 +35,7 @@ export default class Company extends Model {
@attr('string') slug;
/** @dates */
@attr('date') joined_at;
@attr('date') deleted_at;
@attr('date') created_at;
@attr('date') updated_at;
@@ -71,4 +73,32 @@ export default class Company extends Model {
toJSON() {
return autoSerialize(this);
}
async transferOwnership(newOwner, params = {}) {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');
return fetch.post('companies/transfer-ownership', { company: this.id, newOwner, ...params });
}
async leave(user = null, params = {}) {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');
return fetch.post('companies/leave', { company: this.id, user, ...params });
}
async loadUsers(params = {}) {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');
try {
const users = await fetch.get(`companies/${this.id}/users`, { ...params }, { normalizeToEmberData: true, normalizeModelType: 'user' });
this.set('users', users);
return users;
} catch (error) {
this.set('users', []);
return [];
}
}
}

View File

@@ -83,4 +83,14 @@ export default class DashboardModel extends Model {
});
}
}
getRegistry() {
const owner = getOwner(this);
const universe = owner.lookup('service:universe');
if (universe) {
return universe.getDashboardRegistry(this.id);
}
return undefined;
}
}

View File

@@ -23,9 +23,11 @@ export default class UserModel extends Model {
@attr('string') country;
@attr('string') ip_address;
@attr('string') slug;
@attr('string') role_name;
@attr('string') type;
@attr('string') session_status;
@attr('string') status;
@attr('string') locale;
@attr('boolean') is_online;
@attr('boolean') is_admin;
@attr('raw') meta;
@@ -67,6 +69,17 @@ export default class UserModel extends Model {
});
}
verify() {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');
return fetch.patch(`users/verify/${this.id}`).then((response) => {
this.email_verified_at = response.email_verified_at;
return response;
});
}
removeFromCurrentCompany() {
const owner = getOwner(this);
const fetch = owner.lookup('service:fetch');

View File

@@ -7,15 +7,18 @@ export default class Router extends EmberRouter {
}
Router.map(function () {
this.route('virtual', { path: '/:slug' });
this.route('install');
this.route('onboard', function () {
this.route('verify-email');
});
this.route('auth', function () {
this.route('login', { path: '/' });
this.route('forgot-password');
this.route('reset-password', { path: '/reset-password/:id' });
this.route('two-fa');
this.route('verification');
});
this.route('onboard', function () {
this.route('verify-email');
this.route('portal-login', { path: '/portal' });
});
this.route('invite', { path: 'join' }, function () {
this.route('for-driver', { path: '/fleet/:public_id' });
@@ -23,24 +26,23 @@ Router.map(function () {
});
this.route('console', { path: '/' }, function () {
this.route('home', { path: '/' });
this.route('extensions');
this.route('notifications');
this.route('account', function () {
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('auth');
});
this.route('settings', function () {
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('two-fa');
});
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('admin', function () {
this.route('config', function () {
this.route('database');
this.route('cache');
this.route('filesystem');
this.route('mail');
this.route('notification-channels');
this.route('notification-channels', { path: '/push-notifications' });
this.route('queue');
this.route('services');
this.route('socket');
@@ -48,12 +50,16 @@ Router.map(function () {
this.route('branding');
this.route('notifications');
this.route('two-fa-settings');
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('organizations', function () {
this.route('index', { path: '/' });
this.route('users', { path: '/:company_id' });
this.route('index', { path: '/' }, function () {
this.route('users', { path: '/:public_id/users' });
});
});
this.route('schedule-monitor', function () {
this.route('logs', { path: '/:id/logs' });
});
});
});
this.route('install');
this.route('catch', { path: '/*' });
});

View File

@@ -1,6 +1,7 @@
import Route from '@ember/routing/route';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import isElectron from '@fleetbase/ember-core/utils/is-electron';
import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
@@ -11,9 +12,41 @@ export default class ApplicationRoute extends Route {
@service urlSearchParams;
@service modalsManager;
@service intl;
@service currentUser;
@service router;
@service universe;
@tracked defaultTheme;
/**
* Handle the transition into the application.
*
* @memberof ApplicationRoute
*/
@action willTransition(transition) {
this.universe.callHooks('application:will-transition', this.session, this.router, transition);
}
/**
* On application route activation
*
* @memberof ApplicationRoute
* @void
*/
@action activate() {
this.initializeTheme();
this.initializeLocale();
}
/**
* The application loading event.
* Here will just run extension hooks.
*
* @memberof ApplicationRoute
*/
@action loading(transition) {
this.universe.callHooks('application:loading', this.session, this.router, transition);
}
/**
* Check the installation status of Fleetbase and transition user accordingly.
*
@@ -43,24 +76,27 @@ export default class ApplicationRoute extends Route {
* @return {Transition}
* @memberof ApplicationRoute
*/
async beforeModel() {
async beforeModel(transition) {
await this.session.setup();
await this.universe.booting();
this.universe.callHooks('application:before-model', this.session, this.router, transition);
const { isAuthenticated } = this.session;
const shift = this.urlSearchParams.get('shift');
if (isAuthenticated && shift) {
if (this.session.isAuthenticated && shift) {
return this.router.transitionTo(pathToRoute(shift));
}
}
/**
* On application route activation
* Initializes the application's theme settings, applying necessary class names and default theme configurations.
*
* @memberof ApplicationRoute
* @void
* This method prepares the theme by setting up an array of class names that should be applied to the
* application's body element. If the application is running inside an Electron environment, it adds the
* `'is-electron'` class to the array. It then calls the `initialize` method of the `theme` service,
* passing in the `bodyClassNames` array and the `defaultTheme` configuration.
*/
activate() {
initializeTheme() {
const bodyClassNames = [];
if (isElectron()) {
@@ -68,7 +104,18 @@ export default class ApplicationRoute extends Route {
}
this.theme.initialize({ bodyClassNames, theme: this.defaultTheme });
this.intl.setLocale(['en-us']);
}
/**
* Initializes the application's locale settings based on the current user's preferences.
*
* This method retrieves the user's preferred locale using the `getOption` method from the `currentUser` service.
* If no locale is set by the user, it defaults to `'en-us'`. It then sets the application's locale by calling
* the `setLocale` method of the `intl` service with the retrieved locale.
*/
initializeLocale() {
const locale = this.currentUser.getOption('locale', 'en-us');
this.intl.setLocale([locale]);
}
/**

View File

@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
export default class AuthLoginRoute extends Route {
@service session;
@service universe;
/**
* If user is authentication redirect to console.
@@ -10,7 +11,8 @@ export default class AuthLoginRoute extends Route {
* @memberof AuthLoginRoute
* @void
*/
beforeModel() {
beforeModel(transition) {
this.session.prohibitAuthentication('console');
return this.universe.virtualRouteRedirect(transition, 'auth:login', 'virtual', { restoreQueryParams: true });
}
}

View File

@@ -0,0 +1,10 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class CatchRoute extends Route {
@service router;
beforeModel() {
return this.router.transitionTo('auth.login');
}
}

View File

@@ -1,11 +1,14 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import '@fleetbase/leaflet-routing-machine';
export default class ConsoleRoute extends Route {
@service store;
@service session;
@service universe;
@service router;
@service currentUser;
@service intl;
/**
* Require authentication to access all `console` routes.
@@ -14,10 +17,14 @@ export default class ConsoleRoute extends Route {
* @return {Promise}
* @memberof ConsoleRoute
*/
@action async beforeModel(transition) {
this.session.requireAuthentication(transition, 'auth.login');
async beforeModel(transition) {
await this.session.requireAuthentication(transition, 'auth.login');
return this.session.promiseCurrentUser(transition);
this.universe.callHooks('console:before-model', this.session, this.router, transition);
if (this.session.isAuthenticated) {
return this.session.promiseCurrentUser(transition);
}
}
/**

View File

@@ -0,0 +1,10 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleAccountOrganizationsRoute extends Route {
@service currentUser;
model() {
return this.currentUser.loadOrganizations();
}
}

View File

@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
export default class ConsoleAccountVirtualRoute extends Route {
@service universe;
model({ slug, view }) {
return this.universe.lookupMenuItemFromRegistry('account', slug, view);
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:account', slug, view);
}
}

View File

@@ -7,8 +7,7 @@ export default class ConsoleAdminRoute extends Route {
@service router;
beforeModel() {
// USER MUST BE ADMIN
if (!this.currentUser.user.is_admin) {
if (!this.currentUser.isAdmin) {
return this.router.transitionTo('console').then(() => {
this.notifications.error('You do not have authorization to access admin!');
});

View File

@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
export default class ConsoleAdminVirtualRoute extends Route {
@service universe;
model({ slug, view }) {
return this.universe.lookupMenuItemFromRegistry('admin', slug, view);
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:admin', slug, view);
}
}

View File

@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
export default class ConsoleSettingsVirtualRoute extends Route {
@service universe;
model({ slug, view }) {
return this.universe.lookupMenuItemFromRegistry('settings', slug, view);
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:settings', slug, view);
}
}

View File

@@ -4,7 +4,14 @@ import { inject as service } from '@ember/service';
export default class ConsoleVirtualRoute extends Route {
@service universe;
model({ slug, view }) {
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console', slug, view);
}
}

View File

@@ -0,0 +1,17 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class VirtualRoute extends Route {
@service universe;
queryParams = {
view: {
refreshModel: true,
},
};
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('auth:login', slug, view);
}
}

View File

@@ -27,6 +27,14 @@ export default class UserSerializer extends ApplicationSerializer.extend(Embedde
// delete the password always
delete json.password;
// delete verification attributes
delete json.email_verified_at;
delete json.phone_verified_at;
// delete server managed dates
delete json.deleted_at;
delete json.created_at;
delete json.updated_at;
return json;
}

View File

@@ -1,255 +0,0 @@
import Service from '@ember/service';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency-decorators';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
/**
* Service for managing dashboards, including loading, creating, and deleting dashboards, as well as managing the current dashboard and widget states.
* Utilizes Ember services such as `store`, `fetch`, `notifications`, and `universe` for data management and user interaction.
*
* @extends Service
*/
export default class DashboardService extends Service {
/**
* Ember Data store service for managing model data.
* @type {Service}
*/
@service store;
/**
* Fetch service for making network requests.
* @type {Service}
*/
@service fetch;
/**
* Notifications service for displaying user notifications.
* @type {Service}
*/
@service notifications;
/**
* Universe service for accessing global application state or utility methods.
* @type {Service}
*/
@service universe;
/**
* Internationalization service.
* @type {Service}
*/
@service intl;
/**
* Tracked array of available dashboards.
* @type {Array}
*/
@tracked dashboards = [];
/**
* Tracked property representing the currently selected dashboard.
* @type {Object}
*/
@tracked currentDashboard;
/**
* Tracked boolean indicating if the dashboard is in editing mode.
* @type {boolean}
*/
@tracked isEditingDashboard = false;
/**
* Tracked boolean indicating if a widget is being added.
* @type {boolean}
*/
@tracked isAddingWidget = false;
/**
* Task for loading dashboards from the store. It sets the current dashboard and checks if adding widget is necessary.
*/
@task *loadDashboards() {
const dashboards = yield this.store.findAll('dashboard');
if (isArray(dashboards)) {
this.dashboards = dashboards.toArray();
// insert default dashboard if it's not loaded
const defaultDashboard = this._createDefaultDashboard();
if (this._isDefaultDashboardNotLoaded()) {
this.dashboards.unshiftObject(defaultDashboard);
}
// Set the current dashboard
this.currentDashboard = this._getNextDashboard();
if (this.currentDashboard && this.currentDashboard.widgets.length === 0) {
this.onAddingWidget(true);
}
}
}
/**
* Task for selecting a dashboard. Handles dashboard switching and updates the current dashboard.
* @param {Object} dashboard - The dashboard object to select.
*/
@task *selectDashboard(dashboard) {
if (dashboard.user_uuid === 'system') {
this.currentDashboard = dashboard;
yield this.fetch.post('dashboards/reset-default');
return;
}
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
this.notifications.serverError(error);
});
if (currentDashboard) {
this.currentDashboard = currentDashboard;
}
}
/**
* Task for creating a new dashboard. It handles dashboard creation, success notification, and dashboard selection.
* @param {string} name - Name of the new dashboard.
*/
@task *createDashboard(name) {
const dashboardRecord = this.store.createRecord('dashboard', { name, is_default: true });
const dashboard = yield dashboardRecord.save().catch((error) => {
this.notifications.serverError(error);
});
if (dashboard) {
this.notifications.success(this.intl.t('services.dashboard-service.create-dashboard-success-notification', { dashboardName: dashboard.name }));
this.selectDashboard.perform(dashboard);
this.dashboards.pushObject(dashboard);
}
}
/**
* Task for deleting a dashboard. Handles dashboard deletion and success notification.
* @param {Object} dashboard - The dashboard object to delete.
* @param {Object} [options={}] - Optional configuration options.
*/
@task *deleteDashboard(dashboard, options = {}) {
yield dashboard.destroyRecord().catch((error) => {
this.notification.serverError(error);
if (typeof options.onError === 'function') {
options.onError(error, dashboard);
}
});
this.notifications.success(this.intl.t('services.dashboard-service.delete-dashboard-success-notification', { dashboardName: dashboard.name }));
yield this.loadDashboards.perform();
yield this.selectDashboard.perform(this._getNextDashboard());
if (typeof options.callback === 'function') {
options.callback(this.currentDashboard);
}
}
/**
* Task for setting the current dashboard.
* @param {Object} dashboard - The dashboard object to set as current.
*/
@task *setCurrentDashboard(dashboard) {
const currentDashboard = yield this.fetch.post('dashboards/switch', { dashboard_uuid: dashboard.id }, { normalizeToEmberData: true }).catch((error) => {
this.notifications.serverError(error);
});
if (currentDashboard) {
this.currentDashboard = currentDashboard;
}
}
/**
* Action to toggle dashboard editing state.
* @param {boolean} [state=true] - State to set for editing.
*/
@action onChangeEdit(state = true) {
this.isEditingDashboard = state;
}
/**
* Action to toggle the state of adding a widget.
* @param {boolean} [state=true] - State to set for adding a widget.
*/
@action onAddingWidget(state = true) {
this.isAddingWidget = state;
}
/**
* Creates a default dashboard with predefined widgets.
* @private
* @returns {Object} The default dashboard object.
*/
_createDefaultDashboard() {
let defaultDashboard;
// check store for default dashboard
const loadedDashboars = this.store.peekAll('dashboard');
// check for default dashboard loaded in store
defaultDashboard = loadedDashboars.find((dashboard) => dashboard.id === 'system');
if (defaultDashboard) {
return defaultDashboard;
}
// create new default dashboard
defaultDashboard = this.store.createRecord('dashboard', {
id: 'system',
uuid: 'system',
name: 'Default Dashboard',
is_default: false,
user_uuid: 'system',
widgets: this._createDefaultDashboardWidgets(),
});
return defaultDashboard;
}
/**
* Creates default widgets for the default dashboard.
* @private
* @returns {Array} An array of default dashboard widgets.
*/
_createDefaultDashboardWidgets() {
const widgets = this.universe.getDefaultDashboardWidgets().map((defaultWidget) => {
return this.store.createRecord('dashboard-widget', defaultWidget);
});
return widgets;
}
/**
* Checks if default dashboard is already loaded.
* @private
* @return {Boolean}
* @memberof DashboardService
*/
_isDefaultDashboardLoaded() {
const defaultDashboard = this._createDefaultDashboard();
return this.dashboards.some((dashboard) => dashboard.id === defaultDashboard.id);
}
/**
* Checks if default dashboard is not already loaded.
* @private
* @return {Boolean}
* @memberof DashboardService
*/
_isDefaultDashboardNotLoaded() {
return !this._isDefaultDashboardLoaded();
}
/**
* Gets the current dasbhoard or next available dashboard.
*
* @return {DashboardModel}
* @memberof DashboardService
*/
_getNextDashboard() {
return this.dashboards.find((dashboard) => dashboard.is_default) || this.dashboards[0];
}
}

View File

@@ -13,16 +13,6 @@ body[data-theme='dark'] .two-fa-enforcement-alert button#two-fa-setup-button.btn
cursor: default;
}
.app-version-in-nav {
display: flex;
align-items: center;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
font-size: 0.8rem;
line-height: 1rem;
padding-left: 1rem;
padding-top: 0.2rem;
}
.fleetbase-pagination-meta-info-wrapper.within-layout-section-header {
display: flex;
flex-direction: row;

View File

@@ -1,4 +1,5 @@
{{page-title (t "app.name")}}
<ModalsContainer />
<NotificationContainer @position="top" @zindex="99999" />
<div id="application-root-wormhole"></div>
{{outlet}}

View File

@@ -28,7 +28,7 @@
</p>
</div>
<form class="space-y-6" {{on "submit" this.sendSecureLink}}>
<form class="space-y-6" {{on "submit" (perform this.sendSecureLink)}}>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-50">
{{t "auth.forgot-password.form.email-label"}}

View File

@@ -1,6 +1,6 @@
<div>
<div class="mx-auto w-12 h-12">
<LogoIcon @url={{@brand.icon_url}} @size="12" class="mx-auto" />
<LogoIcon @size="12" class="mx-auto rounded-sm" />
</div>
<h2 class="mt-6 mb-3 text-3xl font-extrabold leading-9 text-center text-gray-900 dark:text-gray-100">
{{t "auth.login.title"}}
@@ -8,16 +8,24 @@
</div>
{{#if (gte this.failedAttempts 3)}}
<div class="px-3 py-2 my-6 rounded-md shadow-sm bg-yellow-200">
<div class="flex mb-5">
<div>
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-900 mr-4" />
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="flex-1">
<p class="flex-1 text-sm text-yellow-100">
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
</p>
</div>
<p class="flex-1 text-sm text-yellow-900 dark:yellow-red-900">
{{t "auth.login.failed-attempt.message" htmlSafe=true}}
</p>
</div>
<Button @text={{t "auth.login.failed-attempt.button-text"}} @type="warning" @onClick={{this.forgotPassword}} />
<Button
@text={{t "auth.login.failed-attempt.button-text"}}
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.forgotPassword}}
/>
</div>
{{/if}}
@@ -65,18 +73,34 @@
href="javascript:;"
{{on "click" this.forgotPassword}}
disabled={{this.isLoading}}
class="font-medium transition duration-150 ease-in-out text-sky-600 hover:text-sky-500 focus:outline-none focus:underline"
class="font-medium transition duration-150 ease-in-out text-sky-500 hover:text-sky-400 focus:outline-none focus:underline"
>
{{t "auth.login.form.forgot-password-label"}}
</a>
</div>
</div>
<div class="mt-6">
<Button @buttonType="submit" @type="primary" @text={{t "auth.login.form.sign-in-button"}} @icon="lock" @wrapperClass="btn-block" @isLoading={{this.isLoading}} @onClick={{this.login}} />
</div>
<div class="mt-3">
<Button @text={{t "auth.login.form.create-account-button"}} @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
<div class="mt-6 space-y-4">
<Button
@buttonType="submit"
@type="primary"
@text={{t "auth.login.form.sign-in-button"}}
@icon="lock"
@wrapperClass="btn-block"
@isLoading={{this.isLoading}}
@onClick={{this.login}}
/>
<Button @text={{t "auth.login.form.create-account-button"}} @icon="briefcase" @wrapperClass="btn-block" @disabled={{this.isLoading}} @onClick={{fn (transition-to "onboard")}} />
<RegistryYield @type="menu" @registry="auth:login" as |menuItem|>
<Button
@text={{menuItem.title}}
@icon={{menuItem.icon}}
@type={{menuItem.type}}
@wrapperClass={{menuItem.wrapperClass}}
@disabled={{this.isLoading}}
@onClick={{menuItem.onClick}}
@permission={{menuItem.permission}}
/>
</RegistryYield>
</div>
</form>

View File

@@ -2,7 +2,7 @@
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-8">
<img class="mx-auto h-12 w-auto " src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}}>
<img class="mx-auto h-12 w-auto" src="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} />
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "auth.verification.title"}}
</h2>
@@ -13,34 +13,66 @@
<FaIcon @icon="shield-check" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "auth.verification.message-text" htmlSafe=true}}
{{t "auth.verification.message-text" htmlSafe=true}}
</p>
</div>
<form class="mt-8 space-y-6" {{on "submit" this.verifyCode}}>
<InputGroup @type="tel" @name={{t "auth.verification.verification-input-label"}} @value={{this.code}} @helpText={{t "auth.verification.verification-code-text"}} @inputClass="input-lg" {{on "input" this.validateInput}} {{did-insert this.validateInitInput}} />
<InputGroup
@type="tel"
@name={{t "auth.verification.verification-input-label"}}
@value={{this.code}}
@helpText={{t "auth.verification.verification-code-text"}}
@inputClass="input-lg"
{{on "input" this.validateInput}}
{{did-insert this.validateInitInput}}
/>
<div class="flex flex-row items-center space-x-4">
<Button @icon="check" @iconPrefix="fas" @buttonType="submit" @type="primary" @size="lg" @text="Verify & Continue" @isLoading={{this.isLoading}} @disabled={{this.isNotReadyToSubmit}} @onClick={{this.verifyCode}} />
<a href="#" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "auth.verification.didnt-receive-a-code"}}</a>
<Button
@icon="check"
@iconPrefix="fas"
@buttonType="submit"
@type="primary"
@size="lg"
@text="Verify & Continue"
@isLoading={{this.isLoading}}
@disabled={{this.isNotReadyToSubmit}}
@onClick={{this.verifyCode}}
/>
<a href="javascript:;" {{on "click" this.onDidntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "auth.verification.didnt-receive-a-code"}}</a>
</div>
{{#if this.stillWaiting}}
<div class="bg-yellow-50 rounded shadow-sm border-l-4 border-yellow-400 px-4 py-2">
<div class="flex">
<div class="flex-shrink-0">
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-400" />
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="ml-3 flex items-center">
<span class="text-lg font-extrabold text-yellow-800">{{t "auth.verification.didnt-receive-a-code"}}</span>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
</div>
</div>
</div>
<div class="py-1">
<p class="text-yellow-700 text-sm">{{t "auth.verification.not-sent.alternative-choice"}}</p>
<div class="flex items-center mt-3">
<Button @buttonType="button" @type="warning" @wrapperClass="mr-2" @onClick={{this.resendEmail}} class="btn-warning-alert">{{t "auth.verification.not-sent.resend-email"}}</Button>
<Button @buttonType="button" @type="warning" @onClick={{this.resendBySms}} class="btn-warning-alert">{{t "auth.verification.not-sent.send-by-sms"}}</Button>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendBySms}}
/>
</div>
</div>
{{/if}}

View File

@@ -0,0 +1,2 @@
{{page-title "Catch"}}
{{outlet}}

View File

@@ -3,6 +3,7 @@
<Layout::Sidebar::Panel @open={{true}} @title={{t "common.account"}}>
<Layout::Sidebar::Item @route="console.account.index" @icon="user">Profile</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.account.auth" @icon="key">Auth</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.account.organizations" @icon="building">Organizations</Layout::Sidebar::Item>
{{#each this.universe.accountMenuItems as |menuItem|}}
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.account.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}

View File

@@ -2,7 +2,7 @@
<Layout::Section::Header @title="Account Auth" />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title="Change Password" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form id="change-password-form" aria-label="change-password" {{on "submit" (perform this.changePassword)}}>
@@ -31,4 +31,5 @@
{{/if}}
</div>
</div>
<Spacer @height="500px" />
</Layout::Section::Body>

View File

@@ -0,0 +1,37 @@
{{page-title "Organizations"}}
<Layout::Section::Header @title="Organizations" />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-4">
<div class="flex flex-row justify-end">
<Button @type="primary" @icon="plus" @text="Create Organization" @onClick={{this.createOrganization}} />
</div>
<ContentPanel @title="Your Organizations" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="space-y-2">
{{#each @model as |organization|}}
<div class="grid grid-cols-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 rounded-lg px-3 py-2 items-center">
<div>
<div class="font-semibold">{{organization.name}}</div>
<div>Member Since: {{format-date organization.joined_at}}</div>
</div>
<div class="col-span-2 flex flex-row items-center justify-end space-x-2">
{{#let (eq organization.owner_uuid this.currentUser.id) as |isOwner|}}
<Button @type="danger" @size="xs" @icon="person-walking-arrow-right" @text="Leave" @onClick={{fn this.leaveOrganization organization}} />
{{#unless (eq this.currentUser.companyId organization.id)}}
<Button @size="xs" @icon="shuffle" @text="Switch" @onClick={{fn this.switchOrganization organization}} />
{{/unless}}
{{#if isOwner}}
<Button @size="xs" @icon="pencil" @text="Edit" @onClick={{fn this.editOrganization organization}} />
<Button @type="danger" @size="xs" @icon="trash" @text="Delete" @onClick={{fn this.deleteOrganization organization}} />
{{/if}}
{{/let}}
</div>
</div>
{{/each}}
</div>
</ContentPanel>
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{@model.title}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -35,4 +35,6 @@
</Layout::Sidebar::Panel>
</EmberWormhole>
{{outlet}}
<Layout::Section::Container>
{{outlet}}
</Layout::Section::Container>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.database.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Database />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.filesystem.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Filesystem />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.mail.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Mail />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.notification-channels.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::NotificationChannels />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.queue.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Queue />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.services.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 900}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Services />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{t "console.admin.config.socket.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 900}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<Configure::Socket />
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,7 +2,7 @@
<Layout::Section::Header @title={{t "common.overview"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 800}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<div class="grid grid-cols-3 xs:grid-cols-1 gap-4">
<StatWidget @title={{t "console.admin.index.total-users"}} @value={{@model.total_users}} />
@@ -11,4 +11,5 @@
</div>
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -15,9 +15,8 @@
@paginationMeta={{@model.meta}}
@page={{this.page}}
@onPageChange={{fn (mut this.page)}}
@tfootVerticalOffset="53"
@tfootVerticalOffsetElements=".next-view-section-subheader"
@onRowClick={{this.goToCompany}}
/>
</Layout::Section::Body>
{{outlet}}

View File

@@ -1,5 +1,15 @@
{{page-title (t "common.users")}}
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width="600px" @isResizable={{true}}>
<Overlay
@onLoad={{this.setOverlayContext}}
@onOpen={{this.onOpen}}
@onClose={{this.onClose}}
@onToggle={{this.onToggle}}
@position="right"
@noBackdrop={{true}}
@fullHeight={{true}}
@isResizeble={{true}}
@width="800px"
>
<Overlay::Header @title={{concat this.company.name " - " (t "common.users")}} @hideStatusDot={{true}} @titleWrapperClass="leading-5">
<div class="flex flex-1 justify-end">
<Button @type="default" @icon="times" @helpText={{t "common.close-and-save"}} @onClick={{this.onPressClose}} />
@@ -7,9 +17,17 @@
</Overlay::Header>
<Overlay::Body class="without-padding">
{{!-- template-lint-disable no-unbound --}}
{{! template-lint-disable no-unbound }}
<Layout::Section::Header @title={{t "console.admin.organizations.users.title"}} @searchQuery={{unbound this.nestedQuery}} @onSearch={{this.search}}>
<Pagination @meta={{@model.meta}} @page={{this.nestedPage}} @onPageChange={{fn (mut this.nestedPage)}} @metaInfoClass="hidden" @metaInfoWrapperClass="within-layout-section-header" />
{{#if (gt @model.meta.total this.nestedLimit)}}
<Pagination
@meta={{@model.meta}}
@page={{this.nestedPage}}
@onPageChange={{fn (mut this.nestedPage)}}
@metaInfoClass="hidden"
@metaInfoWrapperClass="within-layout-section-header"
/>
{{/if}}
</Layout::Section::Header>
<Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{@model.title}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -1,7 +1,7 @@
{{page-title "Dashboard"}}
<Layout::Section::Body class="overflow-y-scroll h-full">
<TwoFaEnforcementAlert />
<Dashboard @sidebar={{this.sidebarContext}} />
<Dashboard @sidebar={{this.sidebarContext}} class="flex items-center justify-between mb-4 mt-6 px-14" />
<Spacer @height="300px" />
</Layout::Section::Body>
<div id="console-home-wormhole" />

View File

@@ -1,7 +1,7 @@
<Layout::Section::Header @title={{t "console.settings.index.title"}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title={{t "console.settings.index.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form {{on "submit" this.saveSettings}}>
@@ -65,4 +65,5 @@
</ContentPanel>
</div>
</div>
<Spacer @height="500px" />
</Layout::Section::Body>

View File

@@ -2,7 +2,7 @@
<Layout::Section::Header @title="2FA" />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 500}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title="2FA Settings" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<div class="mb-3">
@@ -33,4 +33,5 @@
</ContentPanel>
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{@model.title}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -2,9 +2,10 @@
<Layout::Section::Header @title={{@model.title}} />
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen" {{increase-height-by 300}}>
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
</div>
</div>
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -1,5 +1,6 @@
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4" {{increase-height-by 300}}>
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
{{outlet}}
</div>
<Spacer @height="300px" />
</div>

View File

@@ -26,21 +26,35 @@
</div>
{{#if this.stillWaiting}}
<div class="bg-yellow-50 rounded shadow-sm border-l-4 border-yellow-400 px-4 py-2">
<div class="flex">
<div class="flex-shrink-0">
<FaIcon @icon="exclamation-triangle" @size="lg" class="text-yellow-400" />
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="ml-3 flex items-center">
<span class="text-lg font-extrabold text-yellow-800">{{t "onboard.verify-email.didnt-receive-a-code"}}</span>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "onboard.verify-email.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "onboard.verify-email.not-sent.alternative-choice" htmlSafe=true}}</div>
</div>
</div>
</div>
<div class="py-1">
<p class="text-yellow-700 text-sm">{{t "onboard.verify-email.not-sent.alternative-choice"}}</p>
<div class="flex items-center mt-3">
<Button @buttonType="button" @type="warning" @wrapperClass="mr-2" @onClick={{this.resendEmail}} class="btn-warning-alert">{{t "onboard.verify-email.not-sent.resend-email"}}</Button>
<Button @buttonType="button" @type="warning" @onClick={{this.resendBySms}} class="btn-warning-alert">{{t "onboard.verify-email.not-sent.send-by-sms"}}</Button>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "onboard.verify-email.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendEmail}}
/>
<Button
@text={{t "onboard.verify-email.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.resendBySms}}
/>
</div>
</div>
{{/if}}

View File

@@ -0,0 +1,2 @@
{{page-title @model.title}}
{{component @model.component params=@model.componentParams}}

View File

@@ -3,9 +3,18 @@ module.exports = function asArray(value) {
return value;
}
if (typeof value === 'string' && value.includes(',')) {
if (typeof value === 'string') {
return value.split(',');
}
try {
let iterable = Array.from(value);
if (Array.isArray(iterable)) {
return iterable;
}
} catch (error) {
return [];
}
return [];
};

View File

@@ -1,6 +1,6 @@
{
"name": "@fleetbase/console",
"version": "0.5.4",
"version": "0.5.15",
"private": true,
"description": "Modular logistics and supply chain operating system (LSOS)",
"repository": "https://github.com/fleetbase/fleetbase",
@@ -21,7 +21,6 @@
"lint:hbs:fix": "ember-template-lint . --fix",
"lint:js": "eslint . --cache",
"lint:js:fix": "eslint . --fix",
"postinstall": "patch-package",
"lint:intl": "fleetbase-intl-lint",
"start": "pnpm run prebuild && ember serve",
"start:dev": "pnpm run prebuild && ember serve --environment development",
@@ -29,23 +28,22 @@
"test:ember": "ember test"
},
"dependencies": {
"@fleetbase/ember-core": "^0.2.17",
"@fleetbase/ember-ui": "^0.2.24",
"@fleetbase/fleetops-engine": "^0.5.6",
"@fleetbase/storefront-engine": "^0.3.14",
"@fleetbase/dev-engine": "^0.2.6",
"@fleetbase/iam-engine": "^0.1.0",
"@fleetbase/registry-bridge-engine": "^0.0.13",
"@fleetbase/fleetops-data": "^0.1.17",
"@fleetbase/leaflet-routing-machine": "^3.2.16",
"@ember/legacy-built-in-components": "^0.4.2",
"@fleetbase/dev-engine": "^0.2.8",
"@fleetbase/ember-core": "^0.2.21",
"@fleetbase/ember-ui": "^0.2.35",
"@fleetbase/fleetops-data": "^0.1.18",
"@fleetbase/fleetops-engine": "^0.5.12",
"@fleetbase/iam-engine": "^0.1.3",
"@fleetbase/leaflet-routing-machine": "^3.2.16",
"@fleetbase/registry-bridge-engine": "^0.0.17",
"@fleetbase/storefront-engine": "^0.3.16",
"@fortawesome/ember-fontawesome": "^2.0.0",
"ember-changeset": "^4.1.2",
"ember-changeset-validations": "^4.1.1",
"ember-composable-helpers": "^5.0.0",
"ember-concurrency": "^3.1.1",
"ember-concurrency-decorators": "^2.0.3",
"ember-gridstack": "^4.0.0",
"ember-intl": "6.3.2",
"ember-math-helpers": "^2.18.2",
"ember-power-select": "^7.2.0",
@@ -53,8 +51,6 @@
"ember-radio-button": "3.0.0-beta.1",
"ember-tag-input": "^3.1.0",
"fleetbase-extensions-indexer": "^0.0.5",
"gridstack": "^7.3.0",
"patch-package": "^8.0.0",
"postcss-at-rules-variables": "^0.3.0",
"postcss-custom-properties": "^12.1.11",
"postcss-nth-list": "^1.0.2"
@@ -95,7 +91,6 @@
"ember-data": "^4.12.8",
"ember-engines": "^0.9.0",
"ember-fetch": "^8.1.2",
"ember-leaflet": "^5.1.3",
"ember-load-initializers": "^2.1.2",
"ember-modifier": "^4.2.0",
"ember-page-title": "^8.2.3",
@@ -114,7 +109,6 @@
"fast-glob": "^3.3.2",
"fs": "0.0.1-security",
"inter-ui": "^3.19.3",
"leaflet": "^1.9.4",
"loader.js": "^4.7.0",
"normalize.css": "^8.0.1",
"postcss": "^8.4.41",
@@ -143,9 +137,9 @@
},
"pnpm": {
"overrides": {
"@fleetbase/ember-core": "^0.2.17",
"@fleetbase/ember-ui": "^0.2.24",
"@fleetbase/fleetops-data": "^0.1.17"
"@fleetbase/ember-core": "^0.2.21",
"@fleetbase/ember-ui": "^0.2.35",
"@fleetbase/fleetops-data": "^0.1.18"
}
},
"prettier": {

View File

@@ -1,11 +0,0 @@
diff --git a/node_modules/ember-gridstack/addon/components/grid-stack.js b/node_modules/ember-gridstack/addon/components/grid-stack.js
index fa51392..fdabb2a 100644
--- a/node_modules/ember-gridstack/addon/components/grid-stack.js
+++ b/node_modules/ember-gridstack/addon/components/grid-stack.js
@@ -133,5 +133,6 @@ export default class GridStackComponent extends Component {
removeWidget(element, removeDOM = false, triggerEvent = true) {
triggerEvent = triggerEvent && !this.isDestroying && !this.isDestroyed;
this.gridStack?.removeWidget(element, removeDOM, triggerEvent);
+ this.gridStack?.compact();
}
}

4710
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,8 @@ function getRouterFileContents() {
(async () => {
const extensions = await getExtensions();
const consoleExtensions = extensions.filter((extension) => !extension.fleetbase || extension.fleetbase.mount !== 'root');
const rootExtensions = extensions.filter((extension) => extension.fleetbase && extension.fleetbase.mount === 'root');
const routerFileContents = getRouterFileContents();
const ast = recast.parse(routerFileContents, { parser: babelParser });
@@ -92,10 +94,9 @@ function getRouterFileContents() {
functionExpression = arg;
}
});
if (functionExpression) {
// Check and add the new engine mounts
extensions.forEach((extension) => {
consoleExtensions.forEach((extension) => {
const mountPath = getExtensionMountPath(extension.name);
let route = mountPath;
@@ -123,8 +124,46 @@ function getRouterFileContents() {
);
}
});
}
}
return false;
// console.log(path.value.callee.property.name);
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
let functionExpression;
path.value.arguments.forEach((arg) => {
if (arg.type === 'FunctionExpression') {
functionExpression = arg;
}
});
if (functionExpression) {
rootExtensions.forEach((extension) => {
const mountPath = getExtensionMountPath(extension.name);
let route = mountPath;
if (extension.fleetbase && extension.fleetbase.route) {
route = extension.fleetbase.route;
}
const isMounted = functionExpression.body.body.some((expressionStatement) => {
return expressionStatement.expression.arguments[0].value === extension.name;
});
if (!isMounted) {
functionExpression.body.body.push(
builders.expressionStatement(
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
builders.literal(extension.name),
builders.objectExpression([
builders.property('init', builders.identifier('as'), builders.literal(route)),
builders.property('init', builders.identifier('path'), builders.literal(route)),
]),
])
)
);
}
});
}
}

View File

@@ -7,15 +7,18 @@ export default class Router extends EmberRouter {
}
Router.map(function () {
this.route('virtual', { path: '/:slug' });
this.route('install');
this.route('onboard', function () {
this.route('verify-email');
});
this.route('auth', function () {
this.route('login', { path: '/' });
this.route('forgot-password');
this.route('reset-password', { path: '/reset-password/:id' });
this.route('two-fa');
this.route('verification');
});
this.route('onboard', function () {
this.route('verify-email');
this.route('portal-login', { path: '/portal' });
});
this.route('invite', { path: 'join' }, function () {
this.route('for-driver', { path: '/fleet/:public_id' });
@@ -25,14 +28,15 @@ Router.map(function () {
this.route('home', { path: '/' });
this.route('notifications');
this.route('account', function () {
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('auth');
this.route('organizations');
});
this.route('settings', function () {
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('two-fa');
});
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('admin', function () {
this.route('config', function () {
this.route('database');
@@ -47,7 +51,7 @@ Router.map(function () {
this.route('branding');
this.route('notifications');
this.route('two-fa-settings');
this.route('virtual', { path: '/:slug/:view' });
this.route('virtual', { path: '/:slug' });
this.route('organizations', function () {
this.route('index', { path: '/' }, function () {
this.route('users', { path: '/:public_id/users' });
@@ -58,5 +62,5 @@ Router.map(function () {
});
});
});
this.route('install');
this.route('catch', { path: '/*' });
});

View File

@@ -3,24 +3,24 @@ import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | dashboard', function (hooks) {
module('Integration | Component | modals/edit-organization', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<Dashboard />`);
await render(hbs`<Modals::EditOrganization />`);
assert.dom(this.element).hasText('');
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<Dashboard>
<Modals::EditOrganization>
template block text
</Dashboard>
</Modals::EditOrganization>
`);
assert.dom(this.element).hasText('template block text');
assert.dom().hasText('template block text');
});
});

View File

@@ -3,24 +3,24 @@ import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | dashboard/create', function (hooks) {
module('Integration | Component | modals/leave-organization', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<Dashboard::Create />`);
await render(hbs`<Modals::LeaveOrganization />`);
assert.dom(this.element).hasText('');
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<Dashboard::Create>
<Modals::LeaveOrganization>
template block text
</Dashboard::Create>
</Modals::LeaveOrganization>
`);
assert.dom(this.element).hasText('template block text');
assert.dom().hasText('template block text');
});
});

View File

@@ -1,17 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Helper | spread-widget-options', function (hooks) {
setupRenderingTest(hooks);
// TODO: Replace this with your real tests.
test('it renders', async function (assert) {
this.set('inputValue', '1234');
await render(hbs`{{spread-widget-options this.inputValue}}`);
assert.dom().hasText('1234');
});
});

View File

@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | console/account/organizations', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:console/account/organizations');
assert.ok(controller);
});
});

View File

@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | console/account/virtual', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:console/account/virtual');
assert.ok(controller);
});
});

View File

@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | console/admin/virtual', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:console/admin/virtual');
assert.ok(controller);
});
});

View File

@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | console/settings/virtual', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:console/settings/virtual');
assert.ok(controller);
});
});

View File

@@ -1,12 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Service | dashboard', function (hooks) {
module('Unit | Controller | console/virtual', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let service = this.owner.lookup('service:dashboard');
assert.ok(service);
let controller = this.owner.lookup('controller:console/virtual');
assert.ok(controller);
});
});

View File

@@ -0,0 +1,39 @@
import Application from '@ember/application';
import config from '@fleetbase/console/config/environment';
import { initialize } from '@fleetbase/console/instance-initializers/load-leaflet';
import { module, test } from 'qunit';
import Resolver from 'ember-resolver';
import { run } from '@ember/runloop';
module('Unit | Instance Initializer | load-leaflet', function (hooks) {
hooks.beforeEach(function () {
this.TestApplication = class TestApplication extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
};
this.TestApplication.instanceInitializer({
name: 'initializer under test',
initialize,
});
this.application = this.TestApplication.create({
autoboot: false,
});
this.instance = this.application.buildInstance();
});
hooks.afterEach(function () {
run(this.instance, 'destroy');
run(this.application, 'destroy');
});
// TODO: Replace this with your real tests.
test('it works', async function (assert) {
await this.instance.boot();
assert.ok(true);
});
});

View File

@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Route | catch', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:catch');
assert.ok(route);
});
});

View File

@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Route | console/account/organizations', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:console/account/organizations');
assert.ok(route);
});
});

View File

@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Route | virtual', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:virtual');
assert.ok(route);
});
});

View File

@@ -0,0 +1,364 @@
app:
name: Fleetbase
terms:
new: جديد
sort: ترتيب
filter: تصفية
columns: أعمدة
settings: إعدادات
home: الصفحة الرئيسية
admin: مشرف
logout: تسجيل الخروج
dashboard: لوحة القيادة
search: بحث
search-input: إدخال البحث
common:
confirm: تأكيد
edit: تحرير
save: حفظ
save-changes: حفظ التغييرات
cancel: إلغاء
2fa-config: تكوين المصادقة الثنائية
account: حساب
admin: مشرف
branding: العلامة التجارية
columns: أعمدة
dashboard: لوحة القيادة
date-of-birth: تاريخ الميلاد
delete: حذف
email: البريد الإلكتروني
filesystem: نظام الملفات
filter: تصفية
home: الصفحة الرئيسية
logout: تسجيل الخروج
mail: بريد
name: اسم
new: جديد
notification-channels: قنوات الإشعارات
notifications: إشعارات
organization: منظمة
organizations: منظمات
overview: نظرة عامة
phone: رقم هاتفك
profile: الملف الشخصي
queue: قائمة الانتظار
save-button-text: حفظ التغييرات
search-input: إدخال البحث
search: بحث
services: خدمات
settings: إعدادات
socket: مقبس
sort: ترتيب
two-factor: المصادقة الثنائية
uploading: جارٍ التحميل...
your-profile: ملفك الشخصي
created-at: تم الإنشاء في
country: البلد
phone-number: الهاتف
status: الحالة
close-and-save: إغلاق وحفظ
users: المستخدمون
changelog: سجل التغييرات
ok: موافق
select-file: اختر ملف
back: رجوع
next: التالي
continue: متابعة
done: تم
export: تصدير
reload: إعادة تحميل
reload-data: إعادة تحميل البيانات
unauthorized: غير مصرح
unauthorized-to: غير مصرح لـ
unauthorized-access: وصول غير مصرح
unauthorized-access-message: وصول غير مصرح، يجب عليك طلب الأذونات للوصول.
permissions-required-for-changes: ليس لديك الأذونات المطلوبة لإجراء التغييرات.
push-notifications: إشعارات الدفع
component:
file:
dropdown-label: إجراءات الملف
import-modal:
loading-message: جارٍ معالجة الاستيراد...
drop-upload: إسقاط للتحميل
invalid: غير صالح
ready-upload: جاهز للتحميل.
upload-spreadsheets: تحميل جداول البيانات
drag-drop: اسحب وأفلت ملفات جداول البيانات في منطقة الإسقاط هذه
button-text: أو اختر جداول البيانات للتحميل
spreadsheets: جداول البيانات
upload-queue: قائمة انتظار التحميل
dropzone:
file: ملف
drop-to-upload: إسقاط للتحميل
invalid: غير صالح
files-ready-for-upload: >-
{numOfFiles} جاهز للتحميل.
upload-images-videos: تحميل الصور ومقاطع الفيديو
upload-documents: تحميل المستندات
upload-documents-files: تحميل المستندات والملفات
upload-avatar-files: تحميل الصور الرمزية المخصصة
dropzone-supported-images-videos: اسحب وأفلت ملفات الصور والفيديو في منطقة الإسقاط هذه
dropzone-supported-avatars: اسحب وأفلت ملفات SVG أو PNG
dropzone-supported-files: اسحب وأفلت الملفات في منطقة الإسقاط هذه
or-select-button-text: أو اختر الملفات للتحميل.
upload-queue: قائمة انتظار التحميل
uploading: جارٍ التحميل...
two-fa-enforcement-alert:
message: لتعزيز أمان حسابك، تتطلب مؤسستك المصادقة الثنائية (2FA). قم بتمكين المصادقة الثنائية في إعدادات حسابك للحصول على طبقة إضافية من الحماية.
button-text: إعداد المصادقة الثنائية
comment-thread:
publish-comment-button-text: نشر التعليق
publish-reply-button-text: نشر الرد
reply-comment-button-text: رد
edit-comment-button-text: تحرير
delete-comment-button-text: حذف
comment-published-ago: >-
{createdAgo} منذ
comment-input-placeholder: أدخل تعليقًا جديدًا...
comment-reply-placeholder: أدخل ردك...
comment-input-empty-notification: لا يمكنك نشر تعليقات فارغة...
comment-min-length-notification: يجب أن يكون التعليق على الأقل 2 حرف
dashboard:
select-dashboard: اختر لوحة القيادة
create-new-dashboard: إنشاء لوحة قيادة جديدة
create-a-new-dashboard: إنشاء لوحة قيادة جديدة
confirm-create-dashboard: إنشاء لوحة القيادة!
edit-layout: تحرير التخطيط
add-widgets: إضافة عناصر واجهة
delete-dashboard: حذف لوحة القيادة
save-dashboard: حفظ لوحة القيادة
you-cannot-delete-this-dashboard: لا يمكنك حذف هذه اللوحة.
are-you-sure-you-want-delete-dashboard: هل أنت متأكد من حذف {dashboardName}؟
dashboard-widget-panel:
widget-name: >-
عنصر واجهة {widgetName}
select-widgets: اختر عناصر الواجهة
close-and-save: إغلاق وحفظ
services:
dashboard-service:
create-dashboard-success-notification: تم إنشاء لوحة القيادة الجديدة `{dashboardName}` بنجاح.
delete-dashboard-success-notification: تم حذف لوحة القيادة `{dashboardName}`.
auth:
verification:
header-title: التحقق من الحساب
title: تحقق من عنوان بريدك الإلكتروني
message-text: <strong>اقتربت من الانتهاء!</strong><br> تحقق من بريدك الإلكتروني للحصول على رمز التحقق.
verification-code-text: أدخل رمز التحقق الذي تلقيته عبر البريد الإلكتروني.
verification-input-label: رمز التحقق
verify-button-text: تحقق واستمر
didnt-receive-a-code: لم تتلق رمزًا بعد؟
not-sent:
message: لم تتلق رمزًا بعد؟
alternative-choice: استخدم الخيارات البديلة أدناه للتحقق من حسابك.
resend-email: إعادة إرسال البريد الإلكتروني
send-by-sms: إرسال عبر الرسائل القصيرة
two-fa:
verify-code:
verification-code: رمز التحقق
check-title: تحقق من بريدك الإلكتروني أو هاتفك
check-subtitle: لقد أرسلنا لك رمز تحقق. أدخل الرمز أدناه لإكمال عملية تسجيل الدخول.
expired-help-text: انتهت صلاحية رمز المصادقة الثنائية الخاص بك. يمكنك طلب رمز آخر إذا كنت بحاجة إلى مزيد من الوقت.
resend-code: إعادة إرسال الرمز
verify-code: تحقق من الرمز
cancel-two-factor: إلغاء المصادقة الثنائية
invalid-session-error-notification: جلسة غير صالحة. حاول مرة أخرى.
verification-successful-notification: تم التحقق بنجاح!
verification-code-expired-notification: انتهت صلاحية رمز التحقق. يرجى طلب رمز جديد.
verification-code-failed-notification: فشل التحقق. حاول مرة أخرى.
resend-code:
verification-code-resent-notification: تم إرسال رمز التحقق الجديد.
verification-code-resent-error-notification: خطأ في إعادة إرسال رمز التحقق. حاول مرة أخرى.
forgot-password:
success-message: تحقق من بريدك الإلكتروني للمتابعة!
is-sent:
title: اقتربت من الانتهاء!
message: <strong>تحقق من بريدك الإلكتروني!</strong><br> لقد أرسلنا لك رابطًا سحريًا إلى بريدك الإلكتروني سيسمح لك بإعادة تعيين كلمة المرور الخاصة بك. ينتهي صلاحية الرابط في 15 دقيقة.
not-sent:
title: هل نسيت كلمة المرور؟
message: <strong>لا تقلق، نحن هنا لمساعدتك.</strong><br> أدخل البريد الإلكتروني الذي تستخدمه لتسجيل الدخول إلى {appName} وسنرسل لك رابطًا آمنًا لإعادة تعيين كلمة المرور الخاصة بك.
form:
email-label: عنوان بريدك الإلكتروني
submit-button: حسنًا، أرسل لي رابطًا سحريًا!
nevermind-button: لا تهتم
login:
title: تسجيل الدخول إلى حسابك
no-identity-notification: هل نسيت إدخال بريدك الإلكتروني؟
no-password-notification: هل نسيت إدخال كلمة المرور الخاصة بك؟
unverified-notification: يجب التحقق من حسابك للمتابعة.
password-reset-required: مطلوب إعادة تعيين كلمة المرور للمتابعة.
failed-attempt:
message: <strong>هل نسيت كلمة المرور الخاصة بك؟</strong><br> انقر على الزر أدناه لإعادة تعيين كلمة المرور الخاصة بك.
button-text: حسنًا، ساعدني في إعادة التعيين!
form:
email-label: عنوان البريد الإلكتروني
password-label: كلمة المرور
remember-me-label: تذكرني
forgot-password-label: هل نسيت كلمة المرور الخاصة بك؟
sign-in-button: تسجيل الدخول
create-account-button: إنشاء حساب جديد
slow-connection-message: تواجه مشكلات في الاتصال.
reset-password:
success-message: تم إعادة تعيين كلمة المرور الخاصة بك! تسجيل الدخول للمتابعة.
invalid-verification-code: هذا الرابط لإعادة تعيين كلمة المرور غير صالح أو منتهي الصلاحية.
title: إعادة تعيين كلمة المرور الخاصة بك
form:
code:
label: رمز إعادة التعيين الخاص بك
help-text: رمز التحقق الذي تلقيته في بريدك الإلكتروني.
password:
label: كلمة مرور جديدة
help-text: أدخل كلمة مرور لا تقل عن 6 أحرف للمتابعة.
confirm-password:
label: تأكيد كلمة المرور الجديدة
help-text: أدخل كلمة مرور لا تقل عن 6 أحرف للمتابعة.
submit-button: إعادة تعيين كلمة المرور
back-button: رجوع
console:
create-or-join-organization:
modal-title: إنشاء أو الانضمام إلى منظمة
join-success-notification: لقد انضممت إلى منظمة جديدة!
create-success-notification: لقد أنشأت منظمة جديدة!
switch-organization:
modal-title: هل أنت متأكد أنك تريد التبديل إلى المنظمة {organizationName}؟
modal-body: من خلال التأكيد، سيظل حسابك مسجلاً الدخول، ولكن سيتم تبديل المنظمة الأساسية الخاصة بك.
modal-accept-button-text: نعم، أريد التبديل إلى المنظمة
success-notification: لقد قمت بتبديل المنظمات
account:
index:
upload-new: تحميل جديد
phone: رقم هاتفك.
photos: الصور
admin:
schedule-monitor:
schedule-monitor: مراقب الجدول
task-logs-for: >-
سجلات المهام لـ:
showing-last-count: عرض آخر {count} سجلات
name: الاسم
type: النوع
timezone: المنطقة الزمنية
last-started: آخر بدء
last-finished: آخر انتهاء
last-failure: آخر فشل
date: التاريخ
memory: الذاكرة
runtime: وقت التشغيل
output: المخرجات
no-output: لا توجد مخرجات
config:
database:
title: تكوين قاعدة البيانات
filesystem:
title: تكوين نظام الملفات
mail:
title: تكوين البريد
notification-channels:
title: تكوين إشعارات الدفع
queue:
title: تكوين قائمة الانتظار
services:
title: تكوين الخدمات
socket:
title: تكوين المقبس
branding:
title: العلامة التجارية
icon-text: أيقونة
upload-new: تحميل جديد
reset-default: إعادة التعيين إلى الافتراضي
logo-text: شعار
theme: السمة الافتراضية
index:
total-users: إجمالي المستخدمين
total-organizations: إجمالي المنظمات
total-transactions: إجمالي المعاملات
notifications:
title: الإشعارات
notification-settings: إعدادات الإشعارات
organizations:
index:
title: المنظمات
owner-name-column: المالك
owner-phone-column: هاتف المالك
owner-email-column: بريد المالك
users-count-column: المستخدمون
phone-column: الهاتف
email-column: البريد الإلكتروني
users:
title: المستخدمون
settings:
index:
title: إعدادات المنظمة
organization-name: اسم المنظمة
organization-description: وصف المنظمة
organization-phone: رقم هاتف المنظمة
organization-currency: عملة المنظمة
organization-id: معرف المنظمة
organization-branding: العلامة التجارية للمنظمة
logo: الشعار
logo-help-text: شعار منظمتك.
upload-new-logo: تحميل شعار جديد
backdrop: الخلفية
backdrop-help-text: لافتة اختيارية أو صورة خلفية لمنظمتك.
upload-new-backdrop: تحميل خلفية جديدة
extensions:
title: الإضافات قادمة قريبًا!
message: يرجى التحقق مرة أخرى في الإصدارات القادمة حيث نستعد لإطلاق مستودع الإضافات والسوق.
notifications:
select-all: تحديد الكل
mark-as-read: تعليم كمقروء
received: >-
تم الاستلام:
message: لا توجد إشعارات لعرضها.
invite:
for-users:
invitation-message: لقد تمت دعوتك للانضمام إلى {companyName}
invitation-sent-message: لقد تمت دعوتك للانضمام إلى منظمة {companyName} على {appName}. لقبول هذه الدعوة، أدخل رمز الدعوة الذي تلقيته عبر البريد الإلكتروني وانقر على متابعة.
invitation-code-sent-text: رمز الدعوة الخاص بك
accept-invitation-text: قبول الدعوة
onboard:
index:
title: أنشئ حسابك
welcome-title: <strong>مرحبًا بك في {companyName}!</strong><br />
welcome-text: أكمل التفاصيل المطلوبة أدناه للبدء.
full-name: الاسم الكامل
full-name-help-text: اسمك الكامل
your-email: عنوان البريد الإلكتروني
your-email-help-text: عنوان بريدك الإلكتروني
phone: رقم الهاتف
phone-help-text: رقم هاتفك
organization-name: اسم المنظمة
organization-help-text: اسم منظمتك، سيتم إدارة جميع خدماتك ومواردك تحت هذه المنظمة، لاحقًا يمكنك إنشاء العديد من المنظمات كما تريد أو تحتاج.
password: أدخل كلمة مرور
password-help-text: كلمة مرورك، تأكد من أنها جيدة.
confirm-password: تأكيد كلمة المرور
confirm-password-help-text: فقط لتأكيد كلمة المرور التي أدخلتها أعلاه.
continue-button-text: متابعة
verify-email:
header-title: التحقق من الحساب
title: تحقق من عنوان بريدك الإلكتروني
message-text: <strong>اقتربت من الانتهاء!</strong><br> تحقق من بريدك الإلكتروني للحصول على رمز التحقق.
verification-code-text: أدخل رمز التحقق الذي تلقيته عبر البريد الإلكتروني.
verification-input-label: رمز التحقق
verify-button-text: تحقق واستمر
didnt-receive-a-code: لم تتلق رمزًا بعد؟
not-sent:
message: لم تتلق رمزًا بعد؟
alternative-choice: استخدم الخيارات البديلة أدناه للتحقق من حسابك.
resend-email: إعادة إرسال البريد الإلكتروني
send-by-sms: إرسال عبر الرسائل القصيرة
install:
installer-header: المثبت
failed-message-sent: فشل التثبيت! انقر على الزر أدناه لإعادة محاولة التثبيت.
retry-install: إعادة محاولة التثبيت
start-install: بدء التثبيت
layout:
header:
menus:
organization:
settings: إعدادات المنظمة
create-or-join: إنشاء أو الانضمام إلى المنظمات
explore-extensions: استكشاف الإضافات
user:
view-profile: عرض الملف الشخصي
keyboard-shortcuts: عرض اختصارات لوحة المفاتيح
changelog: سجل التغييرات

View File

@@ -0,0 +1,357 @@
app:
name: Fleetbase
terms:
new: Novo
sort: Classificar
filter: Filtrar
columns: Colunas
settings: Configurações
home: Início
admin: Admin
logout: Desconectar
dashboard: Painel
search: Pesquisar
search-input: Termo de Pesquisa
common:
confirm: Confirmar
edit: Editar
save: Salvar
save-changes: Salvar Alterações
cancel: Cancelar
2fa-config: Configurações 2FA
account: Conta
admin: Admin
branding: Marca
columns: Colunas
dashboard: Painel
date-of-birth: Data de Nascimento
delete: Deletar
email: Email
filesystem: Sistema de Arquivos
filter: Filtrar
home: Início
logout: Desconectar
mail: Mail (TRADUZIR)
name: Nome
new: Novo
notification-channels: Canais de Notificações
notifications: Notificações
organization: Organização
organizations: Organizações
overview: Visão geral
phone: Número de Telefone
profile: Perfil
queue: Fila
save-button-text: Salvar Alterações
search-input: Termo de Pesquisa
search: Pesquisar
services: Serviços
settings: Configurações
socket: Socket
sort: Classificar
two-factor: Dois Fatores
uploading: Carregando...
your-profile: Seu Perfil
created-at: Criado em
country: País
phone-number: Telefone
status: Status
close-and-save: Fechar e Salvar
users: Usuários
changelog: Registro de Alterações
ok: OK
select-file: Selecionar Arquivo
back: Voltar
next: Próximo
continue: Continuar
done: Concluído
export: Exportar
reload: Recarregar
reload-data: Recarregar dados
component:
file:
dropdown-label: Ações do Arquivo
import-modal:
loading-message: Processando dados...
drop-upload: Arraste para Carregar
invalid: Invalido
ready-upload: Pronto para Carregar.
upload-spreadsheets: Carregar Planilhas
drag-drop: Arraste e solte os arquivos das planilhas nesta área
button-text: ou selecione planilhas para carregar
spreadsheets: Planilhas
upload-queue: Carregar Fila
dropzone:
file: Arquivo
drop-to-upload: Solte para Carregar
invalid: Invalido
files-ready-for-upload: >-
{numOfFiles} pronto fara carregar.
upload-images-videos: Carregar Imagens & Videos
upload-documents: Carregar Documentos
upload-documents-files: Carregar Documentos & Arquivos
upload-avatar-files: Carregar Avatares Customizados
dropzone-supported-images-videos: Arraste e solte arquivos de imagens e videos nesta área
dropzone-supported-avatars: Arraste e solte arquivos SVG ou PNG.
dropzone-supported-files: Arraste e solte arquivos nesta área.
or-select-button-text: or select files to upload.
upload-queue: Carregar Fila
uploading: Carregando...
two-fa-enforcement-alert:
message: Para aumentar a segurança da sua conta, sua organização requer Autenticação de Dois Fatores (2FA). Ative 2FA na configurações da conta para uma camada adicional de proteção.
button-text: Ativar 2FA
comment-thread:
publish-comment-button-text: Publicar comentário
publish-reply-button-text: Publicar resposta
reply-comment-button-text: Reponder
edit-comment-button-text: Editar
delete-comment-button-text: Deletar
comment-published-ago: >-
{createdAgo} atrás
comment-input-placeholder: Digite um novo comentário...
comment-reply-placeholder: Digite sua resposta...
comment-input-empty-notification: Você não pode publicar comentários em branco...
comment-min-length-notification: Comentarários devem ter pelo menos 2 caracteres
dashboard:
select-dashboard: Selecionar Painel
create-new-dashboard: Criar novo Painel
create-a-new-dashboard: Create um novo Painel
confirm-create-dashboard: Criar Painel!
edit-layout: Editar layout
add-widgets: Adicionar widgets (elementos)
delete-dashboard: Deletar Painel
save-dashboard: Salvar Painel
you-cannot-delete-this-dashboard: Você não pode deletar esse painel.
are-you-sure-you-want-delete-dashboard: Você tem certeza que deseja deletar {dashboardName}?
dashboard-widget-panel:
widget-name: >-
{widgetName} Elemento
select-widgets: Selecionar Elementos
close-and-save: Fechar e Salvar
services:
dashboard-service:
create-dashboard-success-notification: Novo painel `{dashboardName}` criado com sucesso.
delete-dashboard-success-notification: Painel `{dashboardName}` foi deletado.
auth:
verification:
header-title: Verificação de Conta
title: Verifique seu endereço de Email.
message-text: <strong>Quase Pronto!</strong><br> Verifique seu email para obter um código de verificação.
verification-code-text: Digite o código de verificação recebido em seu email.
verification-input-label: Código de Verificação
verify-button-text: Verificar & Continuar
didnt-receive-a-code: Ainda não recebeu o código?
not-sent:
message: Ainda não recebeu o código?
alternative-choice: Use opções alternativas abaixo para verificar sua conta.
resend-email: Reenviar Email
send-by-sms: Enviar via SMS
two-fa:
verify-code:
verification-code: Código de Verificação
check-title: Verifique seu Email ou Telefone.
check-subtitle: Nós enviamos um código de verificação. Digite o código abaixo para completar o processo de login.
expired-help-text: Seu código de verificação 2FA expirou. Você pode pedir outro código se precisar de mais tempo.
resend-code: Reenviar Código
verify-code: Verificar Código
cancel-two-factor: Cancelar Dois-Fatores
invalid-session-error-notification: Sessão Inválida. Por favor tente novamente.
verification-successful-notification: Verificação concluída!
verification-code-expired-notification: Código de vericação expirado. Por favor peça um novo.
verification-code-failed-notification: Verificação falhou. Por favor tente novamente.
resend-code:
verification-code-resent-notification: Novo código de verificação enviado.
verification-code-resent-error-notification: Erro ao reenviar o código de verificação. Por favor tente novamente.
forgot-password:
success-message: Verifique seu email para continuar!
is-sent:
title: Quase Pronto!
message: <strong>Verifique seu Email!</strong><br> Nós te enviamos um link mágico para o seu email que vai permitir você redefinir sua senha. O link expira em 15 minutos.
not-sent:
title: Esqueceu sua senha?
message: <strong>Não se preocupe, nós damos um jeito.</strong><br> Digite o email que voce usa para fazer login no {appName} e nós te enviaremos um link seguro para redefinir sua senha.
form:
email-label: Seu endereço de Email
submit-button: OK, me envie o link!
nevermind-button: Deixa para lá.
login:
title: Entre na sua conta!
no-identity-notification: Você se esqueceu de digitar seu email?
no-password-notification: Você se esqueceu de digirar sua senha?
unverified-notification: Sua conta precisa ser verificada para continuar.
failed-attempt:
message: <strong>Esqueceu a senha?</strong><br> Clique no botão abaixo para redefini-la.
button-text: Ok, ajude-me a redefinir!
form:
email-label: Endereço de Email
password-label: Senha
remember-me-label: Lembre de mim
forgot-password-label: Esqueceu sua Senha?
sign-in-button: Entrar
create-account-button: Criar uma Nova Conta
slow-connection-message: Enfrentando problemas de conectividade.
reset-password:
success-message: Sua senha foi redefinida! Faça Login para continuar.
invalid-verification-code: Esse link de redefinição de senha é inválido ou está expirado.
title: Redefina sua senha
form:
code:
label: Seu código de redefinição
help-text: O código que você recebeu em seu email.
password:
label: Nova Senha
help-text: Digite uma senha de pelo menos 6 caracteres para continuar.
confirm-password:
label: Confirme a nova Senha
help-text: Digite uma senha de pelo menos 6 caracteres para continuar.
submit-button: Redefinir Senha.
back-button: Voltar
console:
create-or-join-organization:
modal-title: Criar ou juntar-se a uma Organização
join-success-notification: Você se juntou a uma organização!
create-success-notification: Você criou uma nova organização!
switch-organization:
modal-title: Você tem certeza que deseja trocar de organização para {organizationName}?
modal-body: Ao confirmar, sua conta permanecerá conectada, mas sua organização principal será alterada.
modal-accept-button-text: Sim, quero mudar de organização
success-notification: Você trocou de organização
account:
index:
upload-new: Carregar novo
phone: Seu telefone.
photos: Fotos
admin:
schedule-monitor:
schedule-monitor: Monitor de Cronograma
task-logs-for: >-
Registros de tarefas para:
showing-last-count: Mostrando últimos {count} registros
name: Nome
type: Tipo
timezone: Fuso horário
last-started: Último Iniciado
last-finished: Último Finalizado
last-failure: Última Falha
date: Data
memory: Memoria
runtime: Tempo de execução
output: Saída
no-output: Sem Saída
config:
database:
title: Configuração do Banco de Dados
filesystem:
title: Configuração do Sistema de Arquivos
mail:
title: Configuração do Email
notification-channels:
title: Configuração de Canais de Notificação
queue:
title: Configuração da Fila
services:
title: Configuração de Serviços
socket:
title: Configuração de Soquete
branding:
title: Marca
icon-text: Ícone
upload-new: Carregar novo
reset-default: Restaurar Padrão
logo-text: Logo
theme: Tema Padrão
index:
total-users: Total de Usuários
total-organizations: Total de Organizações
total-transactions: Total de Transações
notifications:
title: Notificações
notification-settings: Configuração de Notificações
organizations:
index:
title: Organizações
owner-name-column: Proprietário
owner-phone-column: Telefone do Proprietário
owner-email-column: Email do Proprietário
users-count-column: Usuários
phone-column: Telefone
email-column: Email
users:
title: Usuários
settings:
index:
title: Configuração da Organização
organization-name: Nome da Organização
organization-description: Descrição da Organização
organization-phone: Número de telefone da Organização
organization-currency: Moeda da Organização
organization-id: ID da Organização
organization-branding: Marca da Organização
logo: Logo
logo-help-text: Logo da sua Organização.
upload-new-logo: Carregar novo logo
backdrop: Imagem de Fundo
backdrop-help-text: Banner opcional ou imagem de fundo para sua organização.
upload-new-backdrop: Carregar nova Imagem de Fundo
extensions:
title: As extensões chegarão em breve!
message: Volte nas próximas versões enquanto nos preparamos para lançar o repositório e o mercado de extensões.
notifications:
select-all: Selecionar todas
mark-as-read: Marcar como lido
received: >-
Recebido:
message: Nenhuma notificação para exibir.
invite:
for-users:
invitation-message: Você foi convidado para entrar em {companyName}
invitation-sent-message: Você foi convidado para participar da organização {companyName} no {appName}. Para aceitar este convite, insira seu código de convite recebido por e-mail e clique em continuar.
invitation-code-sent-text: Seu código de convite
accept-invitation-text: Aceitar convite
onboard:
index:
title: Criar sua conta
welcome-title: <strong>Bem vindo a {companyName}!</strong><br />
welcome-text: Preencha os detalhes necessários abaixo para começar.
full-name: Nome Completo
full-name-help-text: Seu nome completo
your-email: Endereço de Email
your-email-help-text: Seu endereço de email
phone: Telefone
phone-help-text: Seu número de telefone
organization-name: Nome da Organização
organization-help-text: O nome da sua organização, todos os seus serviços e recursos serão gerenciados por esta organização. Mais tarde, você poderá criar quantas organizações quiser ou precisar.
password: Digite a senha
password-help-text: Sua senha, certifique-se de que seja uma boa senha.
confirm-password: Confirme sua Senha
confirm-password-help-text: Apenas para confirmar a senha que você digitou acima.
continue-button-text: Continuar
verify-email:
header-title: Verificação de Conta
title: Verifique seu endereço de Email
message-text: <strong>Quase pronto!</strong><br> Verifique seu e-mail para obter o código de verificação.
verification-code-text: Insira o código de verificação que você recebeu por e-mail.
verification-input-label: Código de Verificação
verify-button-text: Verificar & Continuar
didnt-receive-a-code: Ainda não recebeu o código?
not-sent:
message: Ainda não recebeu o código?
alternative-choice: Use as opções alternativas abaixo para verificar sua conta.
resend-email: Reenviar E-mail
send-by-sms: Enviar por SMS
install:
installer-header: Instalador
failed-message-sent: A instalação falhou! Clique no botão abaixo para tentar instalar novamente.
retry-install: Tentar instalar novamente
start-install: Iniciar instalação
layout:
header:
menus:
organization:
settings: Configurações da Organização
create-or-join: Criar ou entrar em organizações
explore-extensions: Explorar extensões
user:
view-profile: Ver Perfil
keyboard-shortcuts: Mostrar atalhos de teclado
changelog: Registro de alterações

View File

@@ -0,0 +1,363 @@
app:
name: Fleetbase
terms:
new: Mới
sort: Sắp xếp
filter: Lọc
columns: Cột
settings: Cài đặt
home: Trang chủ
admin: Admin
logout: Đăng xuất
dashboard: Bảng điều khiển
search: Tìm kiếm
search-input: Đầu vào tìm kiếm
common:
confirm: Xác nhận
edit: Chỉnh sửa
save: Lưu
save-changes: Lưu thay đổi
cancel: Hủy
2fa-config: Cấu hình 2FA
account: Tài khoản
admin: Admin
branding: Thương hiệu
columns: Cột
dashboard: Bảng điều khiển
date-of-birth: Ngày sinh
delete: Xóa
email: Email
filesystem: Hệ thống tệp
filter: Lọc
home: Trang chủ
logout: Đăng xuất
mail: Mail
name: Tên
new: Mới
notification-channels: Kênh thông báo
notifications: Thông báo
organization: Tổ chức
organizations: Các tổ chức
overview: Tổng quan
phone: Số điện thoại của bạn
profile: Hồ sơ
queue: Hàng đợi
save-button-text: Lưu thay đổi
search-input: Đầu vào tìm kiếm
search: Tìm kiếm
services: Dịch vụ
settings: Cài đặt
socket: Socket
sort: Sắp xếp
two-factor: Hai yếu tố
uploading: Đang tải lên...
your-profile: Hồ sơ của bạn
created-at: Được tạo vào
country: Quốc qua
phone-number: Số điện thoại
status: Trạng thái
close-and-save: Đóng và Lưu
users: Người dùng
changelog: Nhật ký thay đổi
ok: OK
select-file: Chọn tệp
back: Quay lại
next: Tiếp theo
continue: Tiếp tục
done: Đã xong
export: Xuất
reload: Tải lại
reload-data: Tải lại dữ liệu
unauthorized: Không có quyền
unauthorized-to: Không có quyền để
unauthorized-access: Không có quyền truy cập
unauthorized-access-message: Không có quyền truy cập, bạn phải yêu cầu quyền để truy cập.
permissions-required-for-changes: Bạn không có quyền cần thiết để thực hiện thay đổi.
push-notifications: Đẩy thông báo
component:
file:
dropdown-label: Hành động với tệp
import-modal:
loading-message: Đang tiến hành nhập...
drop-upload: Thả để tải lên
invalid: Không hợp lệ
ready-upload: sẵn sàng tải lên.
upload-spreadsheets: Tải lên Bảng tính
drag-drop: Kéo và thả các tệp bảng tính vào vùng thả này
button-text: hoặc chọn bảng tính để tải lên
spreadsheets: bảng tính
upload-queue: Tải lên hàng đợi
dropzone:
file: file
drop-to-upload: Thả để tải lên
invalid: Không hợp lệ
files-ready-for-upload: >-
{numOfFiles} sẵn sàng tải lên.
upload-images-videos: Tải lên hình ảnh và video
upload-documents: Tải lên tài liệu
upload-documents-files: Tải lên tài liệu và tập tin
upload-avatar-files: Tải lên ảnh đại diện tùy chỉnh
dropzone-supported-images-videos: Kéo và thả ảnh hoặc video vào vùng thả này
dropzone-supported-avatars: Kéo và thả tệp SVG hoặc PNG
dropzone-supported-files: Kéo và thả tệp vào vùng thả này
or-select-button-text: hoặc chọn file để tải lên.
upload-queue: Tải lên hàng đợi
uploading: Đang tải lên...
two-fa-enforcement-alert:
message: Để tăng cường bảo mật cho tài khoản của bạn, tổ chức của bạn yêu cầu Xác thực hai yếu tố (2FA). Bật 2FA trong cài đặt tài khoản của bạn để có thêm một lớp bảo vệ.
button-text: Cấu hình 2FA
comment-thread:
publish-comment-button-text: Đăng bình luận
publish-reply-button-text: Đăng phản hồi
reply-comment-button-text: Phản hồi
edit-comment-button-text: Chỉnh sửa
delete-comment-button-text: Xóa
comment-published-ago: >-
{createdAgo} trước
comment-input-placeholder: Nhập một bình luận mới...
comment-reply-placeholder: Nhập phản hồi của bạn...
comment-input-empty-notification: Bạn không thể đăng bình luận trống...
comment-min-length-notification: Bình luận phải chứa ít nhất 2 ký tự
dashboard:
select-dashboard: Chọn Bảng điều khiển
create-new-dashboard: Tạo mới Bảng điều khiển
create-a-new-dashboard: Tạo mới một Bảng điều khiển
confirm-create-dashboard: Tạo Bảng điều khiển!
edit-layout: Chỉnh sửa bố cục
add-widgets: Thêm tiện ích
delete-dashboard: Xóa bảng điều khiển
save-dashboard: Lưu bảng điều khiển
you-cannot-delete-this-dashboard: Bạn không thể xóa bảng điều khiển này.
are-you-sure-you-want-delete-dashboard: Bạn có chắc chắn xóa {dashboardName}?
dashboard-widget-panel:
widget-name: >-
{widgetName} Tiện ích
select-widgets: Chọn tiện ích
close-and-save: Đóng và Lưu
services:
dashboard-service:
create-dashboard-success-notification: Bảng điều khiển `{dashboardName}` đã được tạo thành công.
delete-dashboard-success-notification: Bảng điều khiển `{dashboardName}` đã được xóa.
auth:
verification:
header-title: Xác thực tài khoản
title: Xác thực email của bạn
message-text: <strong>Gần xong rồi!</strong><br> Kiểm tả email của bạn để lấy mã xác thực.
verification-code-text: Nhập mã xác thực bạn vừa nhận qua email.
verification-input-label: Mã xác thực
verify-button-text: Xác thực & Tiếp tục
didnt-receive-a-code: Chưa nhận được mã?
not-sent:
message: Chưa nhận được mã?
alternative-choice: Sử dụng các tùy chọn thay thế bên dưới để xác thực tài khoản của bạn.
resend-email: Gửi lại email
send-by-sms: Gửi qua SMS
two-fa:
verify-code:
verification-code: Mã xác thực
check-title: Kiểm tra email hoặc điện thoại của bạn.
check-subtitle: Chúng tôi đã gửi cho bạn mã xác thực. Nhập mã bên dưới để hoàn tất quá trình đăng nhập.
expired-help-text: Mã xác thực 2FA của bạn đã hết hạn. Bạn có thể yêu cầu mã khác nếu cần thêm thời gian.
resend-code: Gửi lại mã
verify-code: Xác thực mã
cancel-two-factor: Hủy 2 yếu tố
invalid-session-error-notification: Phiên không hợp lệ. Vui lòng thử lại.
verification-successful-notification: Xác thực thành công!
verification-code-expired-notification: Mã xác thực đã hết hạn. Vui lòng yêu cầu một mã mới.
verification-code-failed-notification: Xác thực thất bại. Vui lòng thử lại.
resend-code:
verification-code-resent-notification: Mã xác thực mới đã được gửi.
verification-code-resent-error-notification: Xảy ra lỗi khi gửi lại mã xác thực. Vui lòng thử lại.
forgot-password:
success-message: Kiểm tra email của bạn để tiếp tục!
is-sent:
title: Gần xong rồi!
message: <strong>Kiểm tra email của bạn!</strong><br> Chúng tôi đã gửi cho bạn một liên kết đến email của bạn, cho phép bạn đặt lại mật khẩu. Liên kết sẽ hết hạn sau 15 phút.
not-sent:
title: Quên mật khẩu?
message: <strong>Đừng lo lắng, chúng tôi luôn hỗ trợ bạn.</strong><br> Nhập email bạn sử dụng để đăng nhập {appName} và chúng tôi sẽ gửi cho bạn một liên kết an toàn để đặt lại mật khẩu.
form:
email-label: Địa chỉ email của bạn
submit-button: OK, Gửi cho tôi một liên kết!
nevermind-button: Ok
login:
title: Đăng nhập vào tài khoản của bạn
no-identity-notification: Bạn quên nhập email của bạn?
no-password-notification: Bạn quên nhập mật khẩu của bạn?
unverified-notification: Tài khoản của bạn cần được xác minh để tiếp tục.
failed-attempt:
message: <strong>Quên mật khẩu?</strong><br> Nhấp vào nút bên dưới để đặt lại mật khẩu của bạn.
button-text: Được, giúp tôi thiết lập lại nhé!
form:
email-label: Địa chỉ email
password-label: Mật khẩu
remember-me-label: Ghi nhớ đăng nhập
forgot-password-label: Quên mật khẩu?
sign-in-button: Đăng nhập
create-account-button: Tạo mới tài khoản
slow-connection-message: Đang gặp sự cố kết nối.
reset-password:
success-message: Mật khẩu của bạn đã được đặt lại! Đăng nhập để tiếp tục.
invalid-verification-code: Liên kết đặt lại mật khẩu này không hợp lệ hoặc đã hết hạn.
title: Đặt lại mật khẩu của bạn
form:
code:
label: Mã đặt lại của bạn
help-text: Mã xác thực bạn đã nhận qua email.
password:
label: Mật khẩu mới
help-text: Nhập mật khẩu ít nhất 6 ký tự để tiếp tục.
confirm-password:
label: Xác nhận mật khẩu mới
help-text: Nhập mật khẩu ít nhất 6 ký tự để tiếp tục.
submit-button: Đặt lại mật khẩu
back-button: Quay lại
console:
create-or-join-organization:
modal-title: Tạo mới hoặc tham gia một tổ chức
join-success-notification: Bạn đã tham gia một tổ chức mới!
create-success-notification: Bạn vừa tạo ra một tổ chức mới!
switch-organization:
modal-title: Bạn có chắc chắn muốn chuyển đổi sang tổ chức {organizationName}?
modal-body: Bằng cách xác nhận, tài khoản của bạn sẽ vẫn được đăng nhập, nhưng tổ chức chính của bạn sẽ được chuyển đổi.
modal-accept-button-text: Vâng, tôi muốn chuyển đổi tổ chức
success-notification: Bạn đã chuyển đổi tổ chức
account:
index:
upload-new: Tải lên mới
phone: Số điện thoại của bạn.
photos: ảnh
admin:
schedule-monitor:
schedule-monitor: Lịch trình giám sát
task-logs-for: >-
Nhật ký tác vụ cho:
showing-last-count: Đang hiển thị {count} nhật ký cuối
name: Tên
type: Loại
timezone: Múi giờ
last-started: Lần bắt đầu cuối
last-finished: Lần kết thúc cuối
last-failure: Lần thất bại cuối
date: Ngày
memory: Memory
runtime: Runtime
output: Đầu ra
no-output: Không có đầu ra
config:
database:
title: Cấu hình cơ sở dữ liệu
filesystem:
title: Cấu hình hệ thống tập tin
mail:
title: Cấu hình Mail
notification-channels:
title: Cấu hình kênh thông báo
queue:
title: Cấu hình hàng đợi
services:
title: Cấu hình dịch vụ
socket:
title: Cấu hình Socket
branding:
title: Thương hiệu
icon-text: Icon
upload-new: Tải lên
reset-default: Đặt về mặc định
logo-text: Logo
theme: Chủ đề mặc định
index:
total-users: Tổng số người dùng
total-organizations: Tổng số tổ chức
total-transactions: Tổng số giao dịch
notifications:
title: Thông báo
notification-settings: Cài đặt thông báo
organizations:
index:
title: Tổ chức
owner-name-column: Người sở hữu
owner-phone-column: Điện thoại chủ sở hữu
owner-email-column: Email chủ sở hữu
users-count-column: Người dùng
phone-column: Số điện thoại
email-column: Email
users:
title: Người dùng
settings:
index:
title: Cài đặt tổ chức
organization-name: Tên tổ chức
organization-description: Mô tả
organization-phone: Số điện thoại
organization-currency: Tiền tệ
organization-id: ID
organization-branding: Thương hiệu
logo: Logo
logo-help-text: Logo cho tổ chức của bạn..
upload-new-logo: Tải lên logo mới
backdrop: Phông nền
backdrop-help-text: Biểu ngữ hoặc hình ảnh nền tùy chọn cho tổ chức của bạn.
upload-new-backdrop: Tải lên phông nền mới
extensions:
title: Các tiện ích mở rộng sẽ sớm ra mắt!
message: Vui lòng kiểm tra lại trong các phiên bản sắp tới khi chúng tôi chuẩn bị ra mắt kho lưu trữ và thị trường Tiện ích mở rộng.
notifications:
select-all: Chọn tất cả
mark-as-read: Đánh dấu là đã đọc
received: >-
Đã nhận:
message: Không có thông báo nào để hiển thị.
invite:
for-users:
invitation-message: Bạn đã được mời tham gia {companyName}
invitation-sent-message: Bạn đã được mời tham gia tổ chức {companyName} trên {appName}. Để chấp nhận lời mời này, hãy nhập mã mời nhận được qua email và nhấp vào tiếp tục.
invitation-code-sent-text: Mã mời của bạn
accept-invitation-text: Chấp nhận lời mời
onboard:
index:
title: Tạo tài khoản mới
welcome-title: <strong>Chào mừng tới {companyName}!</strong><br />
welcome-text: Hoàn tất các thông tin yêu cầu bên dưới để bắt đầu.
full-name: Tên đầy đủ
full-name-help-text: Tên đầy đủ của bạn
your-email: Địa chỉ email
your-email-help-text: Địa chỉ email của bạn
phone: Số điện thoại
phone-help-text: Số điện thoại của bạn
organization-name: Tên tổ chức
organization-help-text: Tên tổ chức của bạn, tất cả các dịch vụ và tài nguyên của bạn sẽ được quản lý theo tổ chức này, sau đó bạn có thể tạo nhiều tổ chức tùy ý hoặc theo nhu cầu.
password: Nhập mật khẩu
password-help-text: Mật khẩu của bạn phải an toàn.
confirm-password: Xác nhận mật khẩu của bạn
confirm-password-help-text: Chỉ để xác nhận mật khẩu bạn đã nhập ở trên.
continue-button-text: Tiếp tục
verify-email:
header-title: Xác thực tài khoản
title: Xác thực địa chỉ email của bạn
message-text: <strong>Gần xong rồi!</strong><br> Kiêm tra mã xác thực trong email của bạn.
verification-code-text: Nhập mã xác thực bạn vừa nhận qua email.
verification-input-label: Mã xác thực
verify-button-text: Xác thực & Tiếp tục
didnt-receive-a-code: Bạn vẫn chưa nhận được mã?
not-sent:
message: Bạn vẫn chưa nhận được mã?
alternative-choice: Sử dụng các tùy chọn thay thế bên dưới để xác thực tài khoản của bạn.
resend-email: Gửi lại
send-by-sms: Gửi qua SMS
install:
installer-header: Trình cài đặt
failed-message-sent: Cài đặt thất bại! Nhấp vào nút bên dưới để thử cài đặt lại.
retry-install: Thử lại cài đặt
start-install: Bắt đầu cài đặt
layout:
header:
menus:
organization:
settings: Cài đặt tổ chức
create-or-join: Tạo mới hoặc tham gia một tổ chức
explore-extensions: Khám phá phần mở rộng
user:
view-profile: Xem hồ sơ
keyboard-shortcuts: Hiển thị phím tắt
changelog: Nhật ký thay đổi

Some files were not shown because too many files have changed in this diff Show More