mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
add is_billable support for create / update project and in timetracker, fixes ST-217
This commit is contained in:
committed by
Constantin Graf
parent
d9244d1ab4
commit
7fb58ea341
@@ -73,3 +73,5 @@ NETWORK_NAME=reverse-proxy-docker-traefik_routing
|
||||
|
||||
FORWARD_DB_PORT=5432
|
||||
FORWARD_WEB_PORT=8083
|
||||
|
||||
PAGINATION_PER_PAGE_DEFAULT=500
|
||||
|
||||
@@ -14,6 +14,7 @@ class OrganizationController extends Controller
|
||||
/**
|
||||
* Get organization
|
||||
*
|
||||
* @operationId getOrganization
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function show(Organization $organization): OrganizationResource
|
||||
@@ -26,6 +27,7 @@ class OrganizationController extends Controller
|
||||
/**
|
||||
* Update organization
|
||||
*
|
||||
* @operationId updateOrganization
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function update(Organization $organization, OrganizationUpdateRequest $request): OrganizationResource
|
||||
|
||||
@@ -25,7 +25,7 @@ class OrganizationResource extends BaseResource
|
||||
'id' => $this->resource->id,
|
||||
/** @var string $name Name */
|
||||
'name' => $this->resource->name,
|
||||
/** @var string $color Personal organizations automatically created after registration */
|
||||
/** @var bool $color Personal organizations automatically created after registration */
|
||||
'is_personal' => $this->resource->personal_team,
|
||||
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||
'billable_rate' => $this->resource->billable_rate,
|
||||
|
||||
@@ -31,6 +31,8 @@ class ProjectResource extends BaseResource
|
||||
'client_id' => $this->resource->client_id,
|
||||
/** @var int|null $billable_rate Billable rate in cents per hour */
|
||||
'billable_rate' => $this->resource->billable_rate,
|
||||
/** @var bool $is_billable Project time entries billable default */
|
||||
'is_billable' => $this->resource->is_billable,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@ const OrganizationResource = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
is_personal: z.string(),
|
||||
is_personal: z.boolean(),
|
||||
billable_rate: z.union([z.number(), z.null()]),
|
||||
})
|
||||
.passthrough();
|
||||
const v1_organizations_update_Body = z
|
||||
const updateOrganization_Body = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
billable_rate: z.union([z.number(), z.null()]).optional(),
|
||||
@@ -69,12 +69,14 @@ const ProjectResource = z
|
||||
color: z.string(),
|
||||
client_id: z.union([z.string(), z.null()]),
|
||||
billable_rate: z.union([z.number(), z.null()]),
|
||||
is_billable: z.boolean(),
|
||||
})
|
||||
.passthrough();
|
||||
const createProject_Body = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
is_billable: z.boolean(),
|
||||
billable_rate: z.union([z.number(), z.null()]).optional(),
|
||||
client_id: z.union([z.string(), z.null()]).optional(),
|
||||
})
|
||||
@@ -188,7 +190,7 @@ export const schemas = {
|
||||
updateMember_Body,
|
||||
MemberResource,
|
||||
OrganizationResource,
|
||||
v1_organizations_update_Body,
|
||||
updateOrganization_Body,
|
||||
ProjectResource,
|
||||
createProject_Body,
|
||||
ProjectMemberResource,
|
||||
@@ -210,7 +212,7 @@ const endpoints = makeApi([
|
||||
{
|
||||
method: 'get',
|
||||
path: '/v1/organizations/:organization',
|
||||
alias: 'v1.organizations.show',
|
||||
alias: 'getOrganization',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
@@ -236,13 +238,13 @@ const endpoints = makeApi([
|
||||
{
|
||||
method: 'put',
|
||||
path: '/v1/organizations/:organization',
|
||||
alias: 'v1.organizations.update',
|
||||
alias: 'updateOrganization',
|
||||
requestFormat: 'json',
|
||||
parameters: [
|
||||
{
|
||||
name: 'body',
|
||||
type: 'Body',
|
||||
schema: v1_organizations_update_Body,
|
||||
schema: updateOrganization_Body,
|
||||
},
|
||||
{
|
||||
name: 'organization',
|
||||
|
||||
@@ -10,7 +10,7 @@ export const test = baseTest.extend<object, { workerStorageState: string }>({
|
||||
await page.getByLabel('Name').fill('John Doe');
|
||||
await page
|
||||
.getByLabel('Email')
|
||||
.fill(`john+${Math.round(Math.random() * 10000)}@doe.com`);
|
||||
.fill(`john+${Math.round(Math.random() * 1000000)}@doe.com`);
|
||||
await page
|
||||
.getByLabel('Password', { exact: true })
|
||||
.fill('amazingpassword123');
|
||||
|
||||
@@ -75,6 +75,7 @@ const inputValue = ref(formatValue(model.value));
|
||||
ref="billableRateInput"
|
||||
v-model="inputValue"
|
||||
@blur="updateRate($event.target.value)"
|
||||
@keydown.enter="updateRate($event.target.value)"
|
||||
type="text"
|
||||
:name="name"
|
||||
placeholder="Billable Rate"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import SelectDropdown from '@/Components/Common/SelectDropdown.vue';
|
||||
import type { BillableKey } from '@/utils/useProjects';
|
||||
import Badge from '@/Components/Common/Badge.vue';
|
||||
import { ChevronDownIcon } from '@heroicons/vue/20/solid';
|
||||
|
||||
const model = defineModel<BillableKey>({
|
||||
default: 'non-billable',
|
||||
});
|
||||
|
||||
type Option = { key: BillableKey; name: string };
|
||||
|
||||
const options: Option[] = [
|
||||
{
|
||||
key: 'non-billable',
|
||||
name: 'Non-billable',
|
||||
},
|
||||
{
|
||||
key: 'default-rate',
|
||||
name: 'Default Rate',
|
||||
},
|
||||
{
|
||||
key: 'custom-rate',
|
||||
name: 'Custom Rate',
|
||||
},
|
||||
];
|
||||
|
||||
function getKeyFromItem(item: Option) {
|
||||
return item.key;
|
||||
}
|
||||
|
||||
function getNameFromItem(item: Option) {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
function getNameForKey(key: BillableKey | undefined) {
|
||||
const item = options.find((item) => getKeyFromItem(item) === key);
|
||||
if (item) {
|
||||
return getNameFromItem(item);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectDropdown
|
||||
v-model="model"
|
||||
:get-key-from-item="getKeyFromItem"
|
||||
:get-name-for-item="getNameFromItem"
|
||||
:items="options">
|
||||
<template #trigger>
|
||||
<Badge size="xlarge" class="bg-input-background cursor-pointer">
|
||||
<span>
|
||||
{{ getNameForKey(model) }}
|
||||
</span>
|
||||
<ChevronDownIcon class="text-muted w-5"></ChevronDownIcon>
|
||||
</Badge>
|
||||
</template>
|
||||
</SelectDropdown>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -13,9 +13,9 @@ import Badge from '@/Components/Common/Badge.vue';
|
||||
import { useClientsStore } from '@/utils/useClients';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import ProjectColorSelector from '@/Components/Common/Project/ProjectColorSelector.vue';
|
||||
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
|
||||
import { UserCircleIcon } from '@heroicons/vue/20/solid';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import ProjectEditBillableSection from '@/Components/Common/Project/ProjectEditBillableSection.vue';
|
||||
|
||||
const { createProject } = useProjectsStore();
|
||||
const { clients } = storeToRefs(useClientsStore());
|
||||
@@ -27,6 +27,7 @@ const project = ref<CreateProjectBody>({
|
||||
color: getRandomColor(),
|
||||
client_id: null,
|
||||
billable_rate: null,
|
||||
is_billable: false,
|
||||
});
|
||||
|
||||
async function submit() {
|
||||
@@ -37,6 +38,7 @@ async function submit() {
|
||||
color: getRandomColor(),
|
||||
client_id: null,
|
||||
billable_rate: null,
|
||||
is_billable: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,12 +89,6 @@ const currentClientName = computed(() => {
|
||||
autocomplete="projectName" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:max-w-[120px]">
|
||||
<InputLabel for="billableRate" value="Billable Rate" />
|
||||
<BillableRateInput
|
||||
v-model="project.billable_rate"
|
||||
name="billableRate" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="client" value="Client" />
|
||||
<ClientDropdown class="mt-2" v-model="project.client_id">
|
||||
@@ -112,10 +108,14 @@ const currentClientName = computed(() => {
|
||||
</ClientDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<ProjectEditBillableSection
|
||||
v-model:isBillable="project.is_billable"
|
||||
v-model:billableRate="
|
||||
project.billable_rate
|
||||
"></ProjectEditBillableSection>
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
class="ms-3"
|
||||
:class="{ 'opacity-25': saving }"
|
||||
|
||||
@@ -61,6 +61,7 @@ async function addProjectIfNoneExists() {
|
||||
{
|
||||
name: searchValue.value,
|
||||
color: getRandomColor(),
|
||||
is_billable: false,
|
||||
},
|
||||
{ params: { organization: page.props.auth.user.current_team_id } }
|
||||
);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
|
||||
import ProjectBillableSelect from '@/Components/Common/Project/ProjectBillableSelect.vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import type { BillableKey } from '@/utils/useProjects';
|
||||
|
||||
const billableRateSelect = ref<BillableKey>('non-billable');
|
||||
|
||||
const billableRate = defineModel<number | null>('billableRate');
|
||||
const isBillable = defineModel<boolean>('isBillable');
|
||||
|
||||
onMounted(() => {
|
||||
if (isBillable.value === true) {
|
||||
if (billableRate.value) {
|
||||
billableRateSelect.value = 'custom-rate';
|
||||
} else {
|
||||
billableRateSelect.value = 'default-rate';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(billableRateSelect, () => {
|
||||
if (billableRateSelect.value === 'non-billable') {
|
||||
isBillable.value = false;
|
||||
billableRate.value = null;
|
||||
} else if (billableRateSelect.value === 'default-rate') {
|
||||
isBillable.value = true;
|
||||
billableRate.value = null;
|
||||
} else {
|
||||
isBillable.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
billableRateSelect.value = 'non-billable';
|
||||
|
||||
const billableOptionInfoTexts: { [key in BillableKey]: string } = {
|
||||
'non-billable':
|
||||
'New time entries for this project not be marked billable by default.',
|
||||
'default-rate':
|
||||
'New time entries for this project will be billable at the default rate by default.',
|
||||
'custom-rate':
|
||||
'New time entries for this project will be billable at a custom rate by default.',
|
||||
};
|
||||
|
||||
const billableOptionInfoText = computed(() => {
|
||||
return billableOptionInfoTexts[billableRateSelect.value];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sm:flex items-center space-y-2 sm:space-y-0 sm:space-x-4 pt-6">
|
||||
<div>
|
||||
<InputLabel for="billable" value="Billable Default" />
|
||||
<ProjectBillableSelect
|
||||
v-model="billableRateSelect"
|
||||
class="mt-2"></ProjectBillableSelect>
|
||||
</div>
|
||||
<div
|
||||
class="sm:max-w-[120px]"
|
||||
v-if="billableRateSelect === 'custom-rate'">
|
||||
<InputLabel for="billableRate" value="Billable Rate" />
|
||||
<BillableRateInput v-model="billableRate" name="billableRate" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-muted pt-2 pl-1">
|
||||
<span>
|
||||
<span class="font-semibold"> Info: </span>
|
||||
{{ billableOptionInfoText }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -12,8 +12,8 @@ import { twMerge } from 'tailwind-merge';
|
||||
import Badge from '@/Components/Common/Badge.vue';
|
||||
import { useClientsStore } from '@/utils/useClients';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
|
||||
import ProjectColorSelector from '@/Components/Common/Project/ProjectColorSelector.vue';
|
||||
import ProjectEditBillableSection from '@/Components/Common/Project/ProjectEditBillableSection.vue';
|
||||
|
||||
const { updateProject } = useProjectsStore();
|
||||
const { clients } = storeToRefs(useClientsStore());
|
||||
@@ -29,6 +29,7 @@ const project = ref<CreateProjectBody>({
|
||||
color: props.originalProject.color,
|
||||
client_id: props.originalProject.client_id,
|
||||
billable_rate: props.originalProject.billable_rate,
|
||||
is_billable: props.originalProject.is_billable,
|
||||
});
|
||||
|
||||
async function submit() {
|
||||
@@ -77,11 +78,6 @@ const currentClientName = computed(() => {
|
||||
required
|
||||
autocomplete="projectName" />
|
||||
</div>
|
||||
<div class="sm:max-w-[120px]">
|
||||
<BillableRateInput
|
||||
v-model="project.billable_rate"
|
||||
name="billable_rate" />
|
||||
</div>
|
||||
<div class="">
|
||||
<ClientDropdown v-model="project.client_id">
|
||||
<template #trigger>
|
||||
@@ -98,6 +94,11 @@ const currentClientName = computed(() => {
|
||||
</ClientDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<ProjectEditBillableSection
|
||||
v-model:isBillable="project.is_billable"
|
||||
v-model:billableRate="
|
||||
project.billable_rate
|
||||
"></ProjectEditBillableSection>
|
||||
</template>
|
||||
<template #footer>
|
||||
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||
|
||||
@@ -33,6 +33,17 @@ function deleteProject() {
|
||||
useProjectsStore().deleteProject(props.project.id);
|
||||
}
|
||||
|
||||
const billableRateInfo = computed(() => {
|
||||
if (props.project.is_billable) {
|
||||
if (props.project.billable_rate) {
|
||||
return formatCents(props.project.billable_rate);
|
||||
} else {
|
||||
return 'Default Rate';
|
||||
}
|
||||
}
|
||||
return '--';
|
||||
});
|
||||
|
||||
const showEditProjectModal = ref(false);
|
||||
</script>
|
||||
|
||||
@@ -61,11 +72,7 @@ const showEditProjectModal = ref(false);
|
||||
<div v-else>No client</div>
|
||||
</div>
|
||||
<div class="whitespace-nowrap px-3 py-4 text-sm text-muted">
|
||||
{{
|
||||
project.billable_rate
|
||||
? formatCents(project.billable_rate)
|
||||
: '--'
|
||||
}}
|
||||
{{ billableRateInfo }}
|
||||
</div>
|
||||
<div
|
||||
class="whitespace-nowrap px-3 py-4 text-sm text-muted flex space-x-1 items-center font-medium">
|
||||
|
||||
@@ -115,7 +115,7 @@ watch(open, () => {
|
||||
<template>
|
||||
<Dropdown v-model="open" align="bottom-start" :closeOnContentClick="false">
|
||||
<template #trigger>
|
||||
<slot name="trigger"></slot>
|
||||
<slot name="trigger"> </slot>
|
||||
</template>
|
||||
<template #content>
|
||||
<div ref="dropdownViewport" class="w-60">
|
||||
|
||||
@@ -11,9 +11,10 @@ const props = defineProps<{
|
||||
<div
|
||||
:class="
|
||||
twMerge(
|
||||
'flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out',
|
||||
|
||||
props.selected ? 'bg-accent-300/20' : 'hover:bg-card-background'
|
||||
'flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out cursor-pointer ',
|
||||
props.selected
|
||||
? 'bg-accent-300/20'
|
||||
: 'hover:bg-card-background-active'
|
||||
)
|
||||
">
|
||||
<span>{{ name }}</span>
|
||||
|
||||
@@ -89,6 +89,7 @@ const filteredProjects = computed(() => {
|
||||
value: '',
|
||||
client_id: null,
|
||||
billable_rate: null,
|
||||
is_billable: false,
|
||||
},
|
||||
tasks: [],
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { switchOrganization } from '@/utils/useOrganization';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import TimeTrackerRangeSelector from '@/Components/Common/TimeTracker/TimeTrackerRangeSelector.vue';
|
||||
import { useProjectsStore } from '@/utils/useProjects';
|
||||
|
||||
const page = usePage<{
|
||||
auth: {
|
||||
@@ -48,6 +49,22 @@ onMounted(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
function setBillableDefaultForProject() {
|
||||
const projectssStore = useProjectsStore();
|
||||
const { projects } = storeToRefs(projectssStore);
|
||||
const project = projects.value.find(
|
||||
(project) => project.id === currentTimeEntry.value.project_id
|
||||
);
|
||||
if (project) {
|
||||
currentTimeEntry.value.billable = project.is_billable;
|
||||
}
|
||||
}
|
||||
|
||||
function updateProject() {
|
||||
setBillableDefaultForProject();
|
||||
updateTimeEntry();
|
||||
}
|
||||
|
||||
function updateTimeEntry() {
|
||||
if (currentTimeEntry.value.id) {
|
||||
useCurrentTimeEntryStore().updateTimer();
|
||||
@@ -108,7 +125,7 @@ function switchToTimeEntryOrganization() {
|
||||
<div class="flex items-center justify-between pl-2">
|
||||
<div class="flex items-center w-[130px] sm:w-auto">
|
||||
<TimeTrackerProjectTaskDropdown
|
||||
@changed="updateTimeEntry"
|
||||
@changed="updateProject"
|
||||
v-model:project="currentTimeEntry.project_id"
|
||||
v-model:task="
|
||||
currentTimeEntry.task_id
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import FormSection from '@/Components/FormSection.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import type { UpdateOrganizationBody } from '@/utils/api';
|
||||
import BillableRateInput from '@/Components/Common/BillableRateInput.vue';
|
||||
import { useOrganizationStore } from '@/utils/useOrganization';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const store = useOrganizationStore();
|
||||
const { fetchOrganization, updateOrganization } = store;
|
||||
const { organization } = storeToRefs(store);
|
||||
|
||||
const organizationBody = ref<UpdateOrganizationBody>({
|
||||
name: '',
|
||||
billable_rate: null,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchOrganization();
|
||||
organizationBody.value = {
|
||||
name: organization.value?.name ?? '',
|
||||
billable_rate: organization.value?.billable_rate,
|
||||
};
|
||||
});
|
||||
|
||||
function submit() {
|
||||
updateOrganization(organizationBody.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormSection>
|
||||
<template #title> Billable Rate</template>
|
||||
|
||||
<template #description>
|
||||
Configure the default billable rate for the organization.
|
||||
</template>
|
||||
|
||||
<template #form>
|
||||
<!-- Organization Owner Information -->
|
||||
<div class="col-span-6">
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel
|
||||
for="organizationBillableRate"
|
||||
value="Organization Billable Rate" />
|
||||
<BillableRateInput
|
||||
v-if="organization"
|
||||
v-model="organizationBody.billable_rate"
|
||||
name="organizationBillableRate"></BillableRateInput>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<PrimaryButton @click="submit">Save</PrimaryButton>
|
||||
</template>
|
||||
</FormSection>
|
||||
</template>
|
||||
@@ -8,6 +8,7 @@ import type { Organization } from '@/types/models';
|
||||
import type { Permissions, Role } from '@/types/jetstream';
|
||||
import ImportData from '@/Pages/Teams/Partials/ImportData.vue';
|
||||
import { canUpdateOrganization } from '@/utils/permissions';
|
||||
import OrganizationBillableRate from '@/Pages/Teams/Partials/OrganizationBillableRate.vue';
|
||||
|
||||
defineProps<{
|
||||
team: Organization;
|
||||
@@ -28,6 +29,11 @@ defineProps<{
|
||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||
<UpdateTeamNameForm :team="team" :permissions="permissions" />
|
||||
|
||||
<SectionBorder />
|
||||
<OrganizationBillableRate
|
||||
v-if="canUpdateOrganization()"
|
||||
:team="team" />
|
||||
|
||||
<TeamMemberManager
|
||||
class="mt-10 sm:mt-0"
|
||||
:team="team"
|
||||
|
||||
@@ -108,3 +108,18 @@ export type AggregatedTimeEntriesQueryParams = ZodiosQueryParamsByAlias<
|
||||
SolidTimeApi,
|
||||
'getAggregatedTimeEntries'
|
||||
>;
|
||||
|
||||
export type OrganizationResponse = ZodiosResponseByAlias<
|
||||
SolidTimeApi,
|
||||
'getOrganization'
|
||||
>;
|
||||
|
||||
export type Organization = ZodiosResponseByAlias<
|
||||
SolidTimeApi,
|
||||
'getOrganization'
|
||||
>['data'];
|
||||
|
||||
export type UpdateOrganizationBody = ZodiosBodyByAlias<
|
||||
SolidTimeApi,
|
||||
'updateOrganization'
|
||||
>;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import { initializeStores } from '@/utils/init';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import type {
|
||||
Organization,
|
||||
OrganizationResponse,
|
||||
UpdateOrganizationBody,
|
||||
} from '@/utils/api';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { api } from '../../../openapi.json.client';
|
||||
|
||||
export function switchOrganization(organizationId: string) {
|
||||
router.put(
|
||||
@@ -15,3 +25,49 @@ export function switchOrganization(organizationId: string) {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const useOrganizationStore = defineStore('organization', () => {
|
||||
const organizationResponse = ref<OrganizationResponse | null>(null);
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
|
||||
async function fetchOrganization() {
|
||||
const organization = getCurrentOrganizationId();
|
||||
if (organization) {
|
||||
organizationResponse.value = await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.getOrganization({
|
||||
params: {
|
||||
organization: organization,
|
||||
},
|
||||
}),
|
||||
undefined,
|
||||
'Failed to fetch organization'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateOrganization(
|
||||
organizationBody: UpdateOrganizationBody
|
||||
) {
|
||||
const organization = getCurrentOrganizationId();
|
||||
if (organization) {
|
||||
await handleApiRequestNotifications(
|
||||
() =>
|
||||
api.updateOrganization(organizationBody, {
|
||||
params: {
|
||||
organization: organization,
|
||||
},
|
||||
}),
|
||||
'Organization updated successfully',
|
||||
'Failed to update organization'
|
||||
);
|
||||
await fetchOrganization();
|
||||
}
|
||||
}
|
||||
|
||||
const organization = computed<Organization | null>(() => {
|
||||
return organizationResponse.value?.data || null;
|
||||
});
|
||||
|
||||
return { organization, fetchOrganization, updateOrganization };
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
import { getCurrentOrganizationId } from '@/utils/useUser';
|
||||
import { useNotificationsStore } from '@/utils/notification';
|
||||
|
||||
export type BillableKey = 'non-billable' | 'default-rate' | 'custom-rate';
|
||||
|
||||
export const useProjectsStore = defineStore('projects', () => {
|
||||
const projectResponse = ref<ProjectResponse | null>(null);
|
||||
const { handleApiRequestNotifications } = useNotificationsStore();
|
||||
|
||||
Reference in New Issue
Block a user