mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-17 04:32:15 +01:00
add frontend format support for currencies, add currencies endpoint
This commit is contained in:
committed by
Constantin Graf
parent
c1d43bcc67
commit
f69bc28316
35
app/Http/Controllers/Api/V1/CurrencyController.php
Normal file
35
app/Http/Controllers/Api/V1/CurrencyController.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Service\CurrencyService;
|
||||||
|
use Brick\Money\ISOCurrencyProvider;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class CurrencyController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get all currencies
|
||||||
|
*
|
||||||
|
* @response array{code: string, name: string, symbol: string}[]
|
||||||
|
* @operationId getCurrencies
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$currencyService = app(CurrencyService::class);
|
||||||
|
|
||||||
|
$currencies = array_values(array_map(
|
||||||
|
fn ($currency) => [
|
||||||
|
'code' => $currency->getCurrencyCode(),
|
||||||
|
'name' => $currency->getName(),
|
||||||
|
'symbol' => $currencyService->getCurrencySymbol($currency->getCurrencyCode()),
|
||||||
|
],
|
||||||
|
ISOCurrencyProvider::getInstance()->getAvailableCurrencies()
|
||||||
|
));
|
||||||
|
|
||||||
|
return response()->json($currencies);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Organization;
|
namespace App\Http\Resources\V1\Organization;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
use App\Http\Resources\V1\BaseResource;
|
use App\Http\Resources\V1\BaseResource;
|
||||||
use App\Models\Organization;
|
use App\Models\Organization;
|
||||||
|
use App\Service\CurrencyService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,7 +40,9 @@ class OrganizationResource extends BaseResource
|
|||||||
*/
|
*/
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
return [
|
$currencyService = app(CurrencyService::class);
|
||||||
|
|
||||||
|
return [
|
||||||
/** @var string $id ID */
|
/** @var string $id ID */
|
||||||
'id' => $this->resource->id,
|
'id' => $this->resource->id,
|
||||||
/** @var string $name Name */
|
/** @var string $name Name */
|
||||||
@@ -47,15 +55,17 @@ class OrganizationResource extends BaseResource
|
|||||||
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
|
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
|
||||||
/** @var string $currency Currency code (ISO 4217) */
|
/** @var string $currency Currency code (ISO 4217) */
|
||||||
'currency' => $this->resource->currency,
|
'currency' => $this->resource->currency,
|
||||||
/** @var string $number_format Number format */
|
/** @var string $currency_symbol Currency symbol */
|
||||||
|
'currency_symbol' => $currencyService->getCurrencySymbol($this->resource->currency),
|
||||||
|
/** @var NumberFormat $number_format Number format */
|
||||||
'number_format' => $this->resource->number_format->value,
|
'number_format' => $this->resource->number_format->value,
|
||||||
/** @var string $currency_format Currency format */
|
/** @var CurrencyFormat $currency_format Currency format */
|
||||||
'currency_format' => $this->resource->currency_format->value,
|
'currency_format' => $this->resource->currency_format->value,
|
||||||
/** @var string $date_format Date format */
|
/** @var DateFormat $date_format Date format */
|
||||||
'date_format' => $this->resource->date_format->value,
|
'date_format' => $this->resource->date_format->value,
|
||||||
/** @var string $interval_format Interval format */
|
/** @var IntervalFormat $interval_format Interval format */
|
||||||
'interval_format' => $this->resource->interval_format->value,
|
'interval_format' => $this->resource->interval_format->value,
|
||||||
/** @var string $time_format Time format */
|
/** @var TimeFormat $time_format Time format */
|
||||||
'time_format' => $this->resource->time_format->value,
|
'time_format' => $this->resource->time_format->value,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Resources\V1\Report;
|
namespace App\Http\Resources\V1\Report;
|
||||||
|
|
||||||
|
use App\Enums\CurrencyFormat;
|
||||||
|
use App\Enums\DateFormat;
|
||||||
|
use App\Enums\IntervalFormat;
|
||||||
|
use App\Enums\NumberFormat;
|
||||||
|
use App\Enums\TimeFormat;
|
||||||
use App\Http\Resources\V1\BaseResource;
|
use App\Http\Resources\V1\BaseResource;
|
||||||
use App\Models\Report;
|
use App\Models\Report;
|
||||||
|
use App\Service\CurrencyService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +70,8 @@ class DetailedWithDataReportResource extends BaseResource
|
|||||||
*/
|
*/
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
|
$currencyService = app(CurrencyService::class);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/** @var string $name Name */
|
/** @var string $name Name */
|
||||||
'name' => $this->resource->name,
|
'name' => $this->resource->name,
|
||||||
@@ -73,16 +81,18 @@ class DetailedWithDataReportResource extends BaseResource
|
|||||||
'public_until' => $this->formatDateTime($this->resource->public_until),
|
'public_until' => $this->formatDateTime($this->resource->public_until),
|
||||||
/** @var string $currency Currency code (ISO 4217) */
|
/** @var string $currency Currency code (ISO 4217) */
|
||||||
'currency' => $this->resource->organization->currency,
|
'currency' => $this->resource->organization->currency,
|
||||||
/** @var string $number_format Number format */
|
/** @var NumberFormat $number_format Number format */
|
||||||
'number_format' => $this->resource->organization->number_format,
|
'number_format' => $this->resource->organization->number_format->value,
|
||||||
/** @var string $currency_format Currency format */
|
/** @var CurrencyFormat $currency_format Currency format */
|
||||||
'currency_format' => $this->resource->organization->currency_format,
|
'currency_format' => $this->resource->organization->currency_format->value,
|
||||||
/** @var string $date_format Date format */
|
/** @var string $currency_symbol Currency symbol */
|
||||||
'date_format' => $this->resource->organization->date_format,
|
'currency_symbol' => $currencyService->getCurrencySymbol($this->resource->organization->currency),
|
||||||
/** @var string $interval_format Interval format */
|
/** @var DateFormat $date_format Date format */
|
||||||
'interval_format' => $this->resource->organization->interval_format,
|
'date_format' => $this->resource->organization->date_format->value,
|
||||||
/** @var string $time_format Time format */
|
/** @var IntervalFormat $interval_format Interval format */
|
||||||
'time_format' => $this->resource->organization->time_format,
|
'interval_format' => $this->resource->organization->interval_format->value,
|
||||||
|
/** @var TimeFormat $time_format Time format */
|
||||||
|
'time_format' => $this->resource->organization->time_format->value,
|
||||||
'properties' => [
|
'properties' => [
|
||||||
/** @var string $group Type of first grouping */
|
/** @var string $group Type of first grouping */
|
||||||
'group' => $this->resource->properties->group->value,
|
'group' => $this->resource->properties->group->value,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect, Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||||
import { test } from '../playwright/fixtures';
|
import { test } from '../playwright/fixtures';
|
||||||
import { formatCents } from '../resources/js/packages/ui/src/utils/money';
|
import { formatCents, getOrganizationCurrencySymbol } from '../resources/js/packages/ui/src/utils/money';
|
||||||
|
import type { CurrencyFormat } from '../resources/js/packages/ui/src/utils/money';
|
||||||
|
|
||||||
async function goToProjectsOverview(page: Page) {
|
async function goToProjectsOverview(page: Page) {
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
|
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
|
||||||
@@ -61,6 +62,12 @@ test('test that updating project member billable rate works for existing time en
|
|||||||
page
|
page
|
||||||
.getByRole('row')
|
.getByRole('row')
|
||||||
.first()
|
.first()
|
||||||
.getByText(formatCents(newBillableRate * 100, 'EUR'))
|
.getByText(formatCents(
|
||||||
|
newBillableRate * 100,
|
||||||
|
'EUR',
|
||||||
|
'symbol-before' as CurrencyFormat,
|
||||||
|
'€',
|
||||||
|
'space-point'
|
||||||
|
))
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { expect, Page } from '@playwright/test';
|
import { expect, Page } from '@playwright/test';
|
||||||
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
||||||
import { test } from '../playwright/fixtures';
|
import { test } from '../playwright/fixtures';
|
||||||
import { formatCents } from '../resources/js/packages/ui/src/utils/money';
|
import { formatCents, getOrganizationCurrencySymbol } from '../resources/js/packages/ui/src/utils/money';
|
||||||
|
import type { CurrencyFormat } from '../resources/js/packages/ui/src/utils/money';
|
||||||
|
|
||||||
async function goToProjectsOverview(page: Page) {
|
async function goToProjectsOverview(page: Page) {
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
|
await page.goto(PLAYWRIGHT_BASE_URL + '/projects');
|
||||||
@@ -131,7 +132,13 @@ test('test that updating billable rate works with existing time entries', async
|
|||||||
page
|
page
|
||||||
.getByRole('row')
|
.getByRole('row')
|
||||||
.first()
|
.first()
|
||||||
.getByText(formatCents(newBillableRate * 100, 'EUR'))
|
.getByText(formatCents(
|
||||||
|
newBillableRate * 100,
|
||||||
|
'EUR',
|
||||||
|
'symbol-before' as CurrencyFormat,
|
||||||
|
'€',
|
||||||
|
'space-point'
|
||||||
|
))
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import BillableRateModal from '@/packages/ui/src/BillableRateModal.vue';
|
import BillableRateModal from '@/packages/ui/src/BillableRateModal.vue';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
|
import { inject, type ComputedRef } from 'vue';
|
||||||
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
const show = defineModel('show', { default: false });
|
const show = defineModel('show', { default: false });
|
||||||
const saving = defineModel('saving', { default: false });
|
const saving = defineModel('saving', { default: false });
|
||||||
|
|
||||||
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
newBillableRate?: number | null;
|
newBillableRate?: number | null;
|
||||||
memberName: string;
|
memberName: string;
|
||||||
@@ -28,7 +32,10 @@ defineEmits<{
|
|||||||
newBillableRate
|
newBillableRate
|
||||||
? formatCents(
|
? formatCents(
|
||||||
newBillableRate,
|
newBillableRate,
|
||||||
getOrganizationCurrencyString()
|
getOrganizationCurrencyString(),
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
)
|
)
|
||||||
: ' the default rate of the organization'
|
: ' the default rate of the organization'
|
||||||
}}</strong
|
}}</strong
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Member } from '@/packages/api/src';
|
import type { Member, Organization } from '@/packages/api/src';
|
||||||
|
import { api } from '@/packages/api/src';
|
||||||
import { CheckCircleIcon, UserCircleIcon } from '@heroicons/vue/20/solid';
|
import { CheckCircleIcon, UserCircleIcon } from '@heroicons/vue/20/solid';
|
||||||
import MemberMoreOptionsDropdown from '@/Components/Common/Member/MemberMoreOptionsDropdown.vue';
|
import MemberMoreOptionsDropdown from '@/Components/Common/Member/MemberMoreOptionsDropdown.vue';
|
||||||
import TableRow from '@/Components/TableRow.vue';
|
import TableRow from '@/Components/TableRow.vue';
|
||||||
import { capitalizeFirstLetter } from '../../../utils/format';
|
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import { api } from '@/packages/api/src';
|
|
||||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
import { canInvitePlaceholderMembers } from '@/utils/permissions';
|
import { canInvitePlaceholderMembers } from '@/utils/permissions';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersStore } from '@/utils/useMembers';
|
||||||
import {computed, ref} from 'vue';
|
import { computed, type ComputedRef, inject, ref } from 'vue';
|
||||||
import MemberEditModal from '@/Components/Common/Member/MemberEditModal.vue';
|
import MemberEditModal from '@/Components/Common/Member/MemberEditModal.vue';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import MemberMergeModal from '@/Components/Common/Member/MemberMergeModal.vue';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import MemberMakePlaceholderModal from '@/Components/Common/Member/MemberMakePlaceholderModal.vue';
|
||||||
import MemberMergeModal from "@/Components/Common/Member/MemberMergeModal.vue";
|
import { capitalizeFirstLetter } from '../../../utils/format';
|
||||||
import MemberMakePlaceholderModal from "@/Components/Common/Member/MemberMakePlaceholderModal.vue";
|
import { formatCents } from '../../../packages/ui/src/utils/money';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
member: Member;
|
member: Member;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
const showEditMemberModal = ref(false);
|
const showEditMemberModal = ref(false);
|
||||||
const showMergeMemberModal = ref(false);
|
const showMergeMemberModal = ref(false);
|
||||||
const showMakeMemberPlaceholderModal = ref(false);
|
const showMakeMemberPlaceholderModal = ref(false);
|
||||||
@@ -35,15 +36,12 @@ async function invitePlaceholder(id: string) {
|
|||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
await handleApiRequestNotifications(
|
await handleApiRequestNotifications(
|
||||||
() =>
|
() =>
|
||||||
api.invitePlaceholder(
|
api.invitePlaceholder(undefined, {
|
||||||
undefined,
|
params: {
|
||||||
{
|
organization: organizationId,
|
||||||
params: {
|
member: id,
|
||||||
organization: organizationId,
|
},
|
||||||
member: id,
|
}),
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
'Member invited successfully',
|
'Member invited successfully',
|
||||||
'Error inviting member'
|
'Error inviting member'
|
||||||
);
|
);
|
||||||
@@ -52,8 +50,7 @@ async function invitePlaceholder(id: string) {
|
|||||||
|
|
||||||
const userHasValidMailAddress = computed(() => {
|
const userHasValidMailAddress = computed(() => {
|
||||||
return !props.member.email.endsWith('@solidtime-import.test');
|
return !props.member.email.endsWith('@solidtime-import.test');
|
||||||
})
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -75,7 +72,10 @@ const userHasValidMailAddress = computed(() => {
|
|||||||
member.billable_rate
|
member.billable_rate
|
||||||
? formatCents(
|
? formatCents(
|
||||||
member.billable_rate,
|
member.billable_rate,
|
||||||
getOrganizationCurrencyString()
|
organization?.currency,
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
)
|
)
|
||||||
: '--'
|
: '--'
|
||||||
}}
|
}}
|
||||||
@@ -101,21 +101,26 @@ const userHasValidMailAddress = computed(() => {
|
|||||||
"
|
"
|
||||||
size="small"
|
size="small"
|
||||||
@click="invitePlaceholder(member.id)"
|
@click="invitePlaceholder(member.id)"
|
||||||
>Invite</SecondaryButton
|
>Invite
|
||||||
>
|
</SecondaryButton>
|
||||||
<MemberMoreOptionsDropdown
|
<MemberMoreOptionsDropdown
|
||||||
:member="member"
|
:member="member"
|
||||||
@edit="showEditMemberModal = true"
|
@edit="showEditMemberModal = true"
|
||||||
@delete="removeMember"
|
@delete="removeMember"
|
||||||
@merge="showMergeMemberModal = true"
|
@merge="showMergeMemberModal = true"
|
||||||
@make-placeholder="showMakeMemberPlaceholderModal = true"
|
@make-placeholder="
|
||||||
></MemberMoreOptionsDropdown>
|
showMakeMemberPlaceholderModal = true
|
||||||
|
"></MemberMoreOptionsDropdown>
|
||||||
</div>
|
</div>
|
||||||
<MemberEditModal
|
<MemberEditModal
|
||||||
v-model:show="showEditMemberModal"
|
v-model:show="showEditMemberModal"
|
||||||
:member="member"></MemberEditModal>
|
:member="member"></MemberEditModal>
|
||||||
<MemberMergeModal v-model:show="showMergeMemberModal" :member="member"></MemberMergeModal>
|
<MemberMergeModal
|
||||||
<MemberMakePlaceholderModal v-model:show="showMakeMemberPlaceholderModal" :member="member"></MemberMakePlaceholderModal>
|
v-model:show="showMergeMemberModal"
|
||||||
|
:member="member"></MemberMergeModal>
|
||||||
|
<MemberMakePlaceholderModal
|
||||||
|
v-model:show="showMakeMemberPlaceholderModal"
|
||||||
|
:member="member"></MemberMakePlaceholderModal>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import BillableRateModal from '@/packages/ui/src/BillableRateModal.vue';
|
import BillableRateModal from '@/packages/ui/src/BillableRateModal.vue';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
|
import { inject, type ComputedRef } from 'vue';
|
||||||
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
const show = defineModel('show', { default: false });
|
const show = defineModel('show', { default: false });
|
||||||
const saving = defineModel('saving', { default: false });
|
const saving = defineModel('saving', { default: false });
|
||||||
|
|
||||||
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
newBillableRate?: number | null;
|
newBillableRate?: number | null;
|
||||||
}>();
|
}>();
|
||||||
@@ -27,7 +31,10 @@ defineEmits<{
|
|||||||
newBillableRate
|
newBillableRate
|
||||||
? formatCents(
|
? formatCents(
|
||||||
newBillableRate,
|
newBillableRate,
|
||||||
getOrganizationCurrencyString()
|
getOrganizationCurrencyString(),
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
)
|
)
|
||||||
: ' none.'
|
: ' none.'
|
||||||
}}</strong
|
}}</strong
|
||||||
|
|||||||
@@ -47,12 +47,17 @@ function archiveProject() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
const billableRateInfo = computed(() => {
|
const billableRateInfo = computed(() => {
|
||||||
if (props.project.is_billable) {
|
if (props.project.is_billable) {
|
||||||
if (props.project.billable_rate) {
|
if (props.project.billable_rate) {
|
||||||
return formatCents(
|
return formatCents(
|
||||||
props.project.billable_rate,
|
props.project.billable_rate,
|
||||||
getOrganizationCurrencyString()
|
getOrganizationCurrencyString(),
|
||||||
|
organization?.value?.currency_format,
|
||||||
|
organization?.value?.currency_symbol,
|
||||||
|
organization?.value?.number_format
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return 'Default Rate';
|
return 'Default Rate';
|
||||||
@@ -63,7 +68,6 @@ const billableRateInfo = computed(() => {
|
|||||||
|
|
||||||
const showEditProjectModal = ref(false);
|
const showEditProjectModal = ref(false);
|
||||||
|
|
||||||
const organization = inject<ComputedRef<Organization>>('organization');
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import BillableRateModal from '@/packages/ui/src/BillableRateModal.vue';
|
import BillableRateModal from '@/packages/ui/src/BillableRateModal.vue';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
|
import { inject, type ComputedRef } from 'vue';
|
||||||
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
const show = defineModel('show', { default: false });
|
const show = defineModel('show', { default: false });
|
||||||
const saving = defineModel('saving', { default: false });
|
const saving = defineModel('saving', { default: false });
|
||||||
|
|
||||||
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
newBillableRate?: number | null;
|
newBillableRate?: number | null;
|
||||||
memberName?: string;
|
memberName?: string;
|
||||||
@@ -28,7 +32,10 @@ defineEmits<{
|
|||||||
newBillableRate
|
newBillableRate
|
||||||
? formatCents(
|
? formatCents(
|
||||||
newBillableRate,
|
newBillableRate,
|
||||||
getOrganizationCurrencyString()
|
getOrganizationCurrencyString(),
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
)
|
)
|
||||||
: ' the default rate of the project'
|
: ' the default rate of the project'
|
||||||
}}</strong
|
}}</strong
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ProjectMember } from '@/packages/api/src';
|
import type { ProjectMember } from '@/packages/api/src';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, inject, type ComputedRef } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import TableRow from '@/Components/TableRow.vue';
|
import TableRow from '@/Components/TableRow.vue';
|
||||||
import { useMembersStore } from '@/utils/useMembers';
|
import { useMembersStore } from '@/utils/useMembers';
|
||||||
@@ -10,10 +10,14 @@ import { formatCents } from '@/packages/ui/src/utils/money';
|
|||||||
import { capitalizeFirstLetter } from '@/utils/format';
|
import { capitalizeFirstLetter } from '@/utils/format';
|
||||||
import ProjectMemberEditModal from '@/Components/Common/ProjectMember/ProjectMemberEditModal.vue';
|
import ProjectMemberEditModal from '@/Components/Common/ProjectMember/ProjectMemberEditModal.vue';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
projectMember: ProjectMember;
|
projectMember: ProjectMember;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
function deleteProjectMember() {
|
function deleteProjectMember() {
|
||||||
useProjectMembersStore().deleteProjectMember(
|
useProjectMembersStore().deleteProjectMember(
|
||||||
props.projectMember.project_id,
|
props.projectMember.project_id,
|
||||||
@@ -51,7 +55,10 @@ const showEditModal = ref(false);
|
|||||||
projectMember.billable_rate
|
projectMember.billable_rate
|
||||||
? formatCents(
|
? formatCents(
|
||||||
projectMember.billable_rate,
|
projectMember.billable_rate,
|
||||||
getOrganizationCurrencyString()
|
getOrganizationCurrencyString(),
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
)
|
)
|
||||||
: '--'
|
: '--'
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import {
|
|||||||
import { FolderIcon } from '@heroicons/vue/16/solid';
|
import { FolderIcon } from '@heroicons/vue/16/solid';
|
||||||
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
|
||||||
import { getOrganizationCurrencyString } from '@/utils/money';
|
import { getOrganizationCurrencyString } from '@/utils/money';
|
||||||
import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
|
import {
|
||||||
|
formatHumanReadableDuration,
|
||||||
|
getDayJsInstance,
|
||||||
|
getLocalizedDayJs,
|
||||||
|
} from '@/packages/ui/src/utils/time';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue';
|
import ReportingTabNavbar from '@/Components/Common/Reporting/ReportingTabNavbar.vue';
|
||||||
import ReportingExportButton from '@/Components/Common/Reporting/ReportingExportButton.vue';
|
import ReportingExportButton from '@/Components/Common/Reporting/ReportingExportButton.vue';
|
||||||
@@ -29,17 +33,13 @@ import ReportSaveButton from '@/Components/Common/Report/ReportSaveButton.vue';
|
|||||||
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
|
import TagDropdown from '@/packages/ui/src/Tag/TagDropdown.vue';
|
||||||
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
||||||
|
|
||||||
import { computed, onMounted, ref, inject, type ComputedRef } from 'vue';
|
import { computed, type ComputedRef, inject, onMounted, ref } from 'vue';
|
||||||
import {
|
|
||||||
getDayJsInstance,
|
|
||||||
getLocalizedDayJs,
|
|
||||||
} from '@/packages/ui/src/utils/time';
|
|
||||||
import { type GroupingOption, useReportingStore } from '@/utils/useReporting';
|
import { type GroupingOption, useReportingStore } from '@/utils/useReporting';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import {
|
import {
|
||||||
type AggregatedTimeEntriesQueryParams,
|
type AggregatedTimeEntriesQueryParams,
|
||||||
type CreateReportBodyProperties,
|
|
||||||
api,
|
api,
|
||||||
|
type CreateReportBodyProperties,
|
||||||
type Organization,
|
type Organization,
|
||||||
} from '@/packages/api/src';
|
} from '@/packages/api/src';
|
||||||
import {
|
import {
|
||||||
@@ -52,6 +52,7 @@ import { useSessionStorage, useStorage } from '@vueuse/core';
|
|||||||
import { useNotificationsStore } from '@/utils/notification';
|
import { useNotificationsStore } from '@/utils/notification';
|
||||||
import type { ExportFormat } from '@/types/reporting';
|
import type { ExportFormat } from '@/types/reporting';
|
||||||
import { getRandomColorWithSeed } from '@/packages/ui/src/utils/color';
|
import { getRandomColorWithSeed } from '@/packages/ui/src/utils/color';
|
||||||
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
|
|
||||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||||
|
|
||||||
@@ -213,7 +214,6 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { getNameForReportingRowEntry, emptyPlaceholder } = useReportingStore();
|
const { getNameForReportingRowEntry, emptyPlaceholder } = useReportingStore();
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
|
||||||
|
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
const { projects } = storeToRefs(projectsStore);
|
const { projects } = storeToRefs(projectsStore);
|
||||||
@@ -450,10 +450,8 @@ const tableData = computed(() => {
|
|||||||
v-for="entry in tableData"
|
v-for="entry in tableData"
|
||||||
:key="entry.description ?? 'none'"
|
:key="entry.description ?? 'none'"
|
||||||
:currency="getOrganizationCurrencyString()"
|
:currency="getOrganizationCurrencyString()"
|
||||||
:entry="entry"
|
:type="aggregatedTableTimeEntries.grouped_type"
|
||||||
:type="
|
:entry="entry"></ReportingRow>
|
||||||
aggregatedTableTimeEntries.grouped_type
|
|
||||||
"></ReportingRow>
|
|
||||||
<div
|
<div
|
||||||
class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
|
class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
|
||||||
<div class="flex items-center pl-6 font-medium">
|
<div class="flex items-center pl-6 font-medium">
|
||||||
@@ -475,7 +473,10 @@ const tableData = computed(() => {
|
|||||||
aggregatedTableTimeEntries.cost
|
aggregatedTableTimeEntries.cost
|
||||||
? formatCents(
|
? formatCents(
|
||||||
aggregatedTableTimeEntries.cost,
|
aggregatedTableTimeEntries.cost,
|
||||||
getOrganizationCurrencyString()
|
getOrganizationCurrencyString(),
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
)
|
)
|
||||||
: '--'
|
: '--'
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -57,7 +57,13 @@ const organization = inject<ComputedRef<Organization>>('organization');
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-end pr-6 flex items-center">
|
<div class="justify-end pr-6 flex items-center">
|
||||||
{{ entry.cost ? formatCents(entry.cost, props.currency) : '--' }}
|
{{ entry.cost ? formatCents(
|
||||||
|
entry.cost,
|
||||||
|
props.currency,
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
|
) : '--' }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
value: string;
|
value?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ defineProps<{
|
|||||||
class="rounded-lg bg-card-background border-card-border shadow-card border px-3.5 py-2.5">
|
class="rounded-lg bg-card-background border-card-border shadow-card border px-3.5 py-2.5">
|
||||||
<dt class="font-semibold text-sm text-text-secondary">{{ title }}</dt>
|
<dt class="font-semibold text-sm text-text-secondary">{{ title }}</dt>
|
||||||
<dd class="text-2xl text-text-primary pt-1 font-semibold">
|
<dd class="text-2xl text-text-primary pt-1 font-semibold">
|
||||||
{{ value }}
|
{{ value ?? '--' }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -283,7 +283,10 @@ const option = computed(() => {
|
|||||||
totalWeeklyBillableAmount
|
totalWeeklyBillableAmount
|
||||||
? formatCents(
|
? formatCents(
|
||||||
totalWeeklyBillableAmount.value,
|
totalWeeklyBillableAmount.value,
|
||||||
getOrganizationCurrencyString()
|
getOrganizationCurrencyString(),
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
)
|
)
|
||||||
: '--'
|
: '--'
|
||||||
" />
|
" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import MainContainer from '@/packages/ui/src/MainContainer.vue';
|
|||||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||||
import { FolderIcon, PlusIcon } from '@heroicons/vue/16/solid';
|
import { FolderIcon, PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, inject, type ComputedRef } from 'vue';
|
||||||
import { useProjectsStore } from '@/utils/useProjects';
|
import { useProjectsStore } from '@/utils/useProjects';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import {
|
import {
|
||||||
@@ -33,9 +33,12 @@ import ProjectEditModal from '@/Components/Common/Project/ProjectEditModal.vue';
|
|||||||
import { Badge } from '@/packages/ui/src';
|
import { Badge } from '@/packages/ui/src';
|
||||||
import { formatCents } from '../packages/ui/src/utils/money';
|
import { formatCents } from '../packages/ui/src/utils/money';
|
||||||
import { getOrganizationCurrencyString } from '../utils/money';
|
import { getOrganizationCurrencyString } from '../utils/money';
|
||||||
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
const { projects } = storeToRefs(useProjectsStore());
|
const { projects } = storeToRefs(useProjectsStore());
|
||||||
|
|
||||||
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
const project = computed(() => {
|
const project = computed(() => {
|
||||||
return (
|
return (
|
||||||
projects.value.find(
|
projects.value.find(
|
||||||
@@ -112,7 +115,10 @@ const shownTasks = computed(() => {
|
|||||||
{{
|
{{
|
||||||
formatCents(
|
formatCents(
|
||||||
project?.billable_rate ?? 0,
|
project?.billable_rate ?? 0,
|
||||||
getOrganizationCurrencyString()
|
getOrganizationCurrencyString(),
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/ h
|
/ h
|
||||||
@@ -145,15 +151,11 @@ const shownTasks = computed(() => {
|
|||||||
<div
|
<div
|
||||||
class="w-full items-center flex justify-between">
|
class="w-full items-center flex justify-between">
|
||||||
<div class="pl-6">
|
<div class="pl-6">
|
||||||
<TabBar
|
<TabBar v-model="activeTab">
|
||||||
v-model="activeTab"
|
<TabBarItem value="active"
|
||||||
>
|
|
||||||
<TabBarItem
|
|
||||||
value="active"
|
|
||||||
>Active
|
>Active
|
||||||
</TabBarItem>
|
</TabBarItem>
|
||||||
<TabBarItem
|
<TabBarItem value="done"
|
||||||
value="done"
|
|
||||||
>Done
|
>Done
|
||||||
</TabBarItem>
|
</TabBarItem>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
@@ -185,11 +187,11 @@ const shownTasks = computed(() => {
|
|||||||
Add Member
|
Add Member
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<ProjectMemberCreateModal
|
<ProjectMemberCreateModal
|
||||||
v-model:show="
|
v-model:show="createProjectMember"
|
||||||
createProjectMember
|
|
||||||
"
|
|
||||||
:project-id="projectId"
|
:project-id="projectId"
|
||||||
:existing-members="projectMembers"></ProjectMemberCreateModal>
|
:existing-members="
|
||||||
|
projectMembers
|
||||||
|
"></ProjectMemberCreateModal>
|
||||||
</template>
|
</template>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { formatHumanReadableDuration } from '@/packages/ui/src/utils/time';
|
|||||||
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
import ReportingRow from '@/Components/Common/Reporting/ReportingRow.vue';
|
||||||
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
import ReportingPieChart from '@/Components/Common/Reporting/ReportingPieChart.vue';
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
|
import type { CurrencyFormat } from '@/packages/ui/src/utils/money';
|
||||||
import { computed, onMounted, provide, ref } from 'vue';
|
import { computed, onMounted, provide, ref } from 'vue';
|
||||||
import { useQuery } from '@tanstack/vue-query';
|
import { useQuery } from '@tanstack/vue-query';
|
||||||
import { api } from '@/packages/api/src';
|
import { api } from '@/packages/api/src';
|
||||||
@@ -55,11 +56,21 @@ const reportNumberFormat = computed(() => {
|
|||||||
return sharedReportResponseData.value?.number_format;
|
return sharedReportResponseData.value?.number_format;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reportCurrencyFormat = computed(() => {
|
||||||
|
return (sharedReportResponseData.value?.currency_format ?? 'symbol-before') as CurrencyFormat;
|
||||||
|
});
|
||||||
|
|
||||||
|
const reportCurrencySymbol = computed(() => {
|
||||||
|
return sharedReportResponseData.value?.currency_symbol;
|
||||||
|
});
|
||||||
|
|
||||||
provide(
|
provide(
|
||||||
'organization',
|
'organization',
|
||||||
computed(() => ({
|
computed(() => ({
|
||||||
'number_format': reportNumberFormat.value,
|
'number_format': reportNumberFormat.value,
|
||||||
'interval_format': reportIntervalFormat.value,
|
'interval_format': reportIntervalFormat.value,
|
||||||
|
'currency_format': reportCurrencyFormat.value,
|
||||||
|
'currency_symbol': reportCurrencySymbol.value,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -217,10 +228,8 @@ onMounted(async () => {
|
|||||||
v-for="entry in tableData"
|
v-for="entry in tableData"
|
||||||
:key="entry.description ?? 'none'"
|
:key="entry.description ?? 'none'"
|
||||||
:currency="reportCurrency"
|
:currency="reportCurrency"
|
||||||
:entry="entry"
|
:currency-format="reportCurrencyFormat"
|
||||||
:type="
|
:entry="entry"></ReportingRow>
|
||||||
aggregatedTableTimeEntries.grouped_type
|
|
||||||
"></ReportingRow>
|
|
||||||
<div
|
<div
|
||||||
class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
|
class="contents [&>*]:transition text-text-tertiary [&>*]:h-[50px]">
|
||||||
<div class="flex items-center pl-6 font-medium">
|
<div class="flex items-center pl-6 font-medium">
|
||||||
@@ -241,7 +250,9 @@ onMounted(async () => {
|
|||||||
{{
|
{{
|
||||||
formatCents(
|
formatCents(
|
||||||
aggregatedTableTimeEntries.cost,
|
aggregatedTableTimeEntries.cost,
|
||||||
reportCurrency
|
reportCurrency,
|
||||||
|
reportCurrencyFormat,
|
||||||
|
reportCurrencySymbol,
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import {
|
|||||||
} from '@/Components/ui/select';
|
} from '@/Components/ui/select';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/vue-query';
|
import { useMutation, useQueryClient } from '@tanstack/vue-query';
|
||||||
import type {
|
import type {
|
||||||
CurrencyFormat,
|
|
||||||
DateFormat,
|
DateFormat,
|
||||||
TimeFormat,
|
TimeFormat,
|
||||||
IntervalFormat,
|
IntervalFormat,
|
||||||
} from '@/packages/ui/src/utils/time';
|
} from '@/packages/ui/src/utils/time';
|
||||||
|
import type { CurrencyFormat } from '@/packages/ui/src/utils/money';
|
||||||
import type { NumberFormat } from '@/packages/ui/src/utils/number';
|
import type { NumberFormat } from '@/packages/ui/src/utils/number';
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
|
|||||||
@@ -167,7 +167,10 @@ export type ApiTokenIndexResponse = ZodiosResponseByAlias<
|
|||||||
'getApiTokens'
|
'getApiTokens'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type CreateApiTokenBody = ZodiosBodyByAlias<SolidTimeApi, 'createApiToken'>;
|
export type CreateApiTokenBody = ZodiosBodyByAlias<
|
||||||
|
SolidTimeApi,
|
||||||
|
'createApiToken'
|
||||||
|
>;
|
||||||
export type ApiToken = ApiTokenIndexResponse['data'][0];
|
export type ApiToken = ApiTokenIndexResponse['data'][0];
|
||||||
|
|
||||||
export type DetailedInvoiceResponse = ZodiosResponseByAlias<
|
export type DetailedInvoiceResponse = ZodiosResponseByAlias<
|
||||||
@@ -175,6 +178,21 @@ export type DetailedInvoiceResponse = ZodiosResponseByAlias<
|
|||||||
'getInvoice'
|
'getInvoice'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type UpdateInvoiceSettings = ZodiosBodyByAlias<
|
||||||
|
SolidTimeApi,
|
||||||
|
'updateInvoiceSettings'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type CreateInvoiceBody = ZodiosBodyByAlias<
|
||||||
|
SolidTimeApi,
|
||||||
|
'createInvoice'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type UpdateInvoiceBody = ZodiosBodyByAlias<
|
||||||
|
SolidTimeApi,
|
||||||
|
'updateInvoice'
|
||||||
|
>;
|
||||||
|
|
||||||
const api = createApiClient('/api', { validate: 'none' });
|
const api = createApiClient('/api', { validate: 'none' });
|
||||||
|
|
||||||
export { createApiClient, api };
|
export { createApiClient, api };
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const InvoiceEntryResource = z
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.union([z.string(), z.null()]),
|
description: z.union([z.string(), z.null()]),
|
||||||
unit_price: z.number().int(),
|
unit_price: z.number().int(),
|
||||||
quantity: z.string(),
|
quantity: z.number(),
|
||||||
order_index: z.number().int(),
|
order_index: z.number().int(),
|
||||||
created_at: z.union([z.string(), z.null()]),
|
created_at: z.union([z.string(), z.null()]),
|
||||||
updated_at: z.union([z.string(), z.null()]),
|
updated_at: z.union([z.string(), z.null()]),
|
||||||
@@ -293,21 +293,6 @@ const MemberMergeIntoRequest = z
|
|||||||
.object({ member_id: z.string() })
|
.object({ member_id: z.string() })
|
||||||
.partial()
|
.partial()
|
||||||
.passthrough();
|
.passthrough();
|
||||||
const OrganizationResource = z
|
|
||||||
.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
is_personal: z.boolean(),
|
|
||||||
billable_rate: z.union([z.number(), z.null()]),
|
|
||||||
employees_can_see_billable_rates: z.boolean(),
|
|
||||||
currency: z.string(),
|
|
||||||
number_format: z.string(),
|
|
||||||
currency_format: z.string(),
|
|
||||||
date_format: z.string(),
|
|
||||||
interval_format: z.string(),
|
|
||||||
time_format: z.string(),
|
|
||||||
})
|
|
||||||
.passthrough();
|
|
||||||
const NumberFormat = z.enum([
|
const NumberFormat = z.enum([
|
||||||
'point-comma',
|
'point-comma',
|
||||||
'comma-point',
|
'comma-point',
|
||||||
@@ -338,6 +323,22 @@ const IntervalFormat = z.enum([
|
|||||||
'hours-minutes-seconds-colon-separated',
|
'hours-minutes-seconds-colon-separated',
|
||||||
]);
|
]);
|
||||||
const TimeFormat = z.enum(['12-hours', '24-hours']);
|
const TimeFormat = z.enum(['12-hours', '24-hours']);
|
||||||
|
const OrganizationResource = z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
is_personal: z.boolean(),
|
||||||
|
billable_rate: z.union([z.number(), z.null()]),
|
||||||
|
employees_can_see_billable_rates: z.boolean(),
|
||||||
|
currency: z.string(),
|
||||||
|
currency_symbol: z.string(),
|
||||||
|
number_format: NumberFormat,
|
||||||
|
currency_format: CurrencyFormat,
|
||||||
|
date_format: DateFormat,
|
||||||
|
interval_format: IntervalFormat,
|
||||||
|
time_format: TimeFormat,
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
const OrganizationUpdateRequest = z
|
const OrganizationUpdateRequest = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().max(255),
|
name: z.string().max(255),
|
||||||
@@ -524,11 +525,12 @@ const DetailedWithDataReportResource = z
|
|||||||
description: z.union([z.string(), z.null()]),
|
description: z.union([z.string(), z.null()]),
|
||||||
public_until: z.union([z.string(), z.null()]),
|
public_until: z.union([z.string(), z.null()]),
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
number_format: z.string(),
|
number_format: NumberFormat,
|
||||||
currency_format: z.string(),
|
currency_format: CurrencyFormat,
|
||||||
date_format: z.string(),
|
currency_symbol: z.string(),
|
||||||
interval_format: z.string(),
|
date_format: DateFormat,
|
||||||
time_format: z.string(),
|
interval_format: IntervalFormat,
|
||||||
|
time_format: TimeFormat,
|
||||||
properties: z
|
properties: z
|
||||||
.object({
|
.object({
|
||||||
group: z.string(),
|
group: z.string(),
|
||||||
@@ -774,12 +776,12 @@ export const schemas = {
|
|||||||
Role,
|
Role,
|
||||||
MemberUpdateRequest,
|
MemberUpdateRequest,
|
||||||
MemberMergeIntoRequest,
|
MemberMergeIntoRequest,
|
||||||
OrganizationResource,
|
|
||||||
NumberFormat,
|
NumberFormat,
|
||||||
CurrencyFormat,
|
CurrencyFormat,
|
||||||
DateFormat,
|
DateFormat,
|
||||||
IntervalFormat,
|
IntervalFormat,
|
||||||
TimeFormat,
|
TimeFormat,
|
||||||
|
OrganizationResource,
|
||||||
OrganizationUpdateRequest,
|
OrganizationUpdateRequest,
|
||||||
ProjectResource,
|
ProjectResource,
|
||||||
ProjectStoreRequest,
|
ProjectStoreRequest,
|
||||||
@@ -817,7 +819,9 @@ const endpoints = makeApi([
|
|||||||
path: '/v1/countries',
|
path: '/v1/countries',
|
||||||
alias: 'getCountries',
|
alias: 'getCountries',
|
||||||
requestFormat: 'json',
|
requestFormat: 'json',
|
||||||
response: z.string(),
|
response: z.array(
|
||||||
|
z.object({ code: z.string(), name: z.string() }).passthrough()
|
||||||
|
),
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
status: 401,
|
status: 401,
|
||||||
@@ -826,6 +830,21 @@ const endpoints = makeApi([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
path: '/v1/currencies',
|
||||||
|
alias: 'getCurrencies',
|
||||||
|
requestFormat: 'json',
|
||||||
|
response: z.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
code: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
symbol: z.string(),
|
||||||
|
})
|
||||||
|
.passthrough()
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/v1/organizations/:organization',
|
path: '/v1/organizations/:organization',
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
|
import TextInput from '@/packages/ui/src/Input/TextInput.vue';
|
||||||
import {
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
formatCents,
|
import { ref, watch, inject, type ComputedRef } from 'vue';
|
||||||
getOrganizationCurrencySymbol,
|
|
||||||
} from '@/packages/ui/src/utils/money';
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
import { useFocus } from '@vueuse/core';
|
import { useFocus } from '@vueuse/core';
|
||||||
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
name: string;
|
name: string;
|
||||||
@@ -13,6 +11,8 @@ const props = defineProps<{
|
|||||||
currency: string;
|
currency: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
const model = defineModel<number | null>({
|
const model = defineModel<number | null>({
|
||||||
default: null,
|
default: null,
|
||||||
});
|
});
|
||||||
@@ -58,9 +58,15 @@ function updateRate(value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatValue(modelValue: number | null) {
|
function formatValue(modelValue: number | null) {
|
||||||
const formattedValue = formatCents(modelValue ?? 0, props.currency);
|
const formattedValue = formatCents(
|
||||||
|
modelValue ?? 0,
|
||||||
|
props.currency,
|
||||||
|
organization?.value?.currency_format,
|
||||||
|
organization?.value?.currency_symbol,
|
||||||
|
organization?.value?.number_format
|
||||||
|
);
|
||||||
return formattedValue
|
return formattedValue
|
||||||
.replace(getOrganizationCurrencySymbol(props.currency), '')
|
?.replace(organization?.value?.currency_symbol ?? '', '')
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { formatCents } from '@/packages/ui/src/utils/money';
|
import { formatCents } from '@/packages/ui/src/utils/money';
|
||||||
import BillableRateModal from '@/packages/ui/src/BillableRateModal.vue';
|
import BillableRateModal from '@/packages/ui/src/BillableRateModal.vue';
|
||||||
|
import { inject, type ComputedRef } from 'vue';
|
||||||
|
import type { Organization } from '@/packages/api/src';
|
||||||
|
|
||||||
const show = defineModel('show', { default: false });
|
const show = defineModel('show', { default: false });
|
||||||
const saving = defineModel('saving', { default: false });
|
const saving = defineModel('saving', { default: false });
|
||||||
|
|
||||||
|
const organization = inject<ComputedRef<Organization>>('organization');
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
newBillableRate?: number | null;
|
newBillableRate?: number | null;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
@@ -26,7 +30,13 @@ defineEmits<{
|
|||||||
The billable rate of {{ projectName }} will be updated to
|
The billable rate of {{ projectName }} will be updated to
|
||||||
<strong>{{
|
<strong>{{
|
||||||
newBillableRate
|
newBillableRate
|
||||||
? formatCents(newBillableRate, currency)
|
? formatCents(
|
||||||
|
newBillableRate,
|
||||||
|
currency,
|
||||||
|
organization?.currency_format,
|
||||||
|
organization?.currency_symbol,
|
||||||
|
organization?.number_format
|
||||||
|
)
|
||||||
: ' the default rate of the organization member'
|
: ' the default rate of the organization member'
|
||||||
}}</strong
|
}}</strong
|
||||||
>.
|
>.
|
||||||
|
|||||||
@@ -1,12 +1,52 @@
|
|||||||
function formatMoney(amount: number, currency: string) {
|
import { formatNumber, type NumberFormat } from './number';
|
||||||
return new Intl.NumberFormat('de-DE', {
|
|
||||||
style: 'currency',
|
export type CurrencyFormat =
|
||||||
currency: currency,
|
| 'iso-code-before-with-space'
|
||||||
}).format(amount);
|
| 'iso-code-after-with-space'
|
||||||
|
| 'symbol-before'
|
||||||
|
| 'symbol-after'
|
||||||
|
| 'symbol-before-with-space'
|
||||||
|
| 'symbol-after-with-space';
|
||||||
|
|
||||||
|
function formatMoney(
|
||||||
|
amount: number,
|
||||||
|
currency?: string,
|
||||||
|
format?: CurrencyFormat,
|
||||||
|
currencySymbol?: string,
|
||||||
|
numberFormat?: NumberFormat
|
||||||
|
) {
|
||||||
|
const formattedAmount = formatNumber(amount, numberFormat);
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'iso-code-before-with-space':
|
||||||
|
return `${currency} ${formattedAmount}`;
|
||||||
|
case 'iso-code-after-with-space':
|
||||||
|
return `${formattedAmount} ${currency}`;
|
||||||
|
case 'symbol-before':
|
||||||
|
return `${currencySymbol}${formattedAmount}`;
|
||||||
|
case 'symbol-after':
|
||||||
|
return `${formattedAmount}${currencySymbol}`;
|
||||||
|
case 'symbol-before-with-space':
|
||||||
|
return `${currencySymbol} ${formattedAmount}`;
|
||||||
|
case 'symbol-after-with-space':
|
||||||
|
return `${formattedAmount} ${currencySymbol}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCents(amount: number, currency: string) {
|
export function formatCents(
|
||||||
return formatMoney(amount / 100, currency);
|
amount: number,
|
||||||
|
currency?: string,
|
||||||
|
format?: CurrencyFormat,
|
||||||
|
currencySymbol?: string,
|
||||||
|
numberFormat?: NumberFormat
|
||||||
|
) {
|
||||||
|
return formatMoney(
|
||||||
|
amount / 100,
|
||||||
|
currency,
|
||||||
|
format,
|
||||||
|
currencySymbol,
|
||||||
|
numberFormat
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOrganizationCurrencySymbol(currency: string) {
|
export function getOrganizationCurrencySymbol(currency: string) {
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ Route::prefix('v1')->name('v1.')->group(static function (): void {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::get('/currencies', [\App\Http\Controllers\Api\V1\CurrencyController::class, 'index'])->name('currencies.index');
|
||||||
|
|
||||||
// Public routes
|
// Public routes
|
||||||
Route::name('public.')->prefix('/public')->group(static function (): void {
|
Route::name('public.')->prefix('/public')->group(static function (): void {
|
||||||
Route::get('/reports', [PublicReportController::class, 'show'])->name('reports.show');
|
Route::get('/reports', [PublicReportController::class, 'show'])->name('reports.show');
|
||||||
|
|||||||
Reference in New Issue
Block a user