Skip to content
Draft
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<template>
<ConfirmModal
ref="deleteAdjustmentModal"
title="Delete adjustment?"
:description="deleteAdjustmentDescription"
proceed-label="Delete"
:markdown="false"
width="36rem"
@proceed="confirmRemoveAdjustment"
/>

<div
class="flex flex-col gap-5 rounded-2xl border border-solid border-surface-4 bg-surface-3 p-6"
>
<h2 class="m-0 text-lg font-semibold text-contrast">Adjustments</h2>

<div v-if="adjustments.length > 0" class="flex flex-col gap-2.5">
<div
v-for="(adjustment, index) in adjustments"
:key="index"
class="grid grid-cols-[minmax(0,1fr)_6.5rem_auto] gap-1.5"
>
<StyledInput
:model-value="adjustment.description"
placeholder="Description"
autocomplete="off"
wrapper-class="w-full"
@update:model-value="updateAdjustment(index, 'description', String($event ?? ''))"
/>
<StyledInput
:model-value="adjustment.amount"
type="number"
inputmode="decimal"
placeholder="0.00"
:step="0.01"
wrapper-class="w-full"
@update:model-value="updateAdjustment(index, 'amount', Number($event ?? 0))"
/>
<ButtonStyled circular type="outlined" color="red">
<button
:aria-label="`Remove adjustment ${index + 1}`"
@click="openDeleteAdjustmentModal(index)"
>
<TrashIcon aria-hidden="true" />
</button>
</ButtonStyled>
</div>
</div>

<ButtonStyled type="outlined">
<button class="w-full" @click="addAdjustment">
<PlusIcon aria-hidden="true" />
Add Adjustment
</button>
</ButtonStyled>
</div>
</template>

<script setup lang="ts">
import { PlusIcon, TrashIcon } from '@modrinth/assets'
import { ButtonStyled, ConfirmModal, StyledInput } from '@modrinth/ui'
import { computed, ref } from 'vue'

import { formatCurrency, type DistributionAdjustment } from '../utils'

const adjustments = defineModel<DistributionAdjustment[]>({ required: true })
const pendingDeleteIndex = ref<number | null>(null)
const deleteAdjustmentModal = ref<InstanceType<typeof ConfirmModal> | null>(null)

const pendingDeleteAdjustment = computed(() =>
pendingDeleteIndex.value === null ? null : adjustments.value[pendingDeleteIndex.value],
)
const deleteAdjustmentDescription = computed(() => {
const adjustment = pendingDeleteAdjustment.value

if (!adjustment) {
return ''
}

if (!adjustment.description) {
return `Delete adjustment for ${formatAdjustmentAmount(adjustment.amount)}`
}

return `Delete adjustment "${adjustment.description}" for ${formatAdjustmentAmount(adjustment.amount)}`
})

function addAdjustment() {
adjustments.value = [...adjustments.value, { description: '', amount: 0 }]
}

function openDeleteAdjustmentModal(index: number) {
pendingDeleteIndex.value = index
deleteAdjustmentModal.value?.show()
}

function confirmRemoveAdjustment() {
if (pendingDeleteIndex.value === null) {
return
}

adjustments.value = adjustments.value.filter(
(_, adjustmentIndex) => adjustmentIndex !== pendingDeleteIndex.value,
)
pendingDeleteIndex.value = null
}

function updateAdjustment(
index: number,
field: keyof DistributionAdjustment,
value: string | number,
) {
adjustments.value = adjustments.value.map((adjustment, adjustmentIndex) =>
adjustmentIndex === index ? { ...adjustment, [field]: value } : adjustment,
)
}

function formatAdjustmentAmount(amount: number): string {
const formatted = formatCurrency(Math.abs(amount), { cents: true })
return amount < 0 ? `-${formatted}` : formatted
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template>
<div class="rounded-2xl border border-solid border-surface-4 bg-surface-3 p-6">
<h2 class="m-0 text-lg font-semibold text-contrast">Distribution Breakdown</h2>

<div class="mt-5 border-0 border-t border-solid border-surface-4 pt-5">
<DistributeBreakdownRow
label="Estimated Revenue"
:value="formatCurrency(estimatedRevenue, { cents: true })"
/>
<DistributeBreakdownRow
label="Clean.io Fee"
:value="formatSignedCurrency(-Math.abs(payout.fees_deducted_usd))"
negative
/>
<DistributeBreakdownRow
label="Variance Deduction"
:value="formatSignedCurrency(payout.variance_adjustment_usd)"
negative
/>
</div>

<div class="mt-4 border-0 border-t border-solid border-surface-4 pt-4">
<DistributeBreakdownRow
label="Net Estimated Revenue"
:value="formatCurrency(payout.net_estimated_revenue_usd, { cents: true })"
strong
/>
</div>

<div class="mt-4 border-0 border-t border-dashed border-surface-4 pt-4">
<DistributeBreakdownRow label="Actual Revenue" :value="actualRevenueLabel" />
<DistributeBreakdownRow label="Variance Resolution" :value="varianceResolutionLabel" />
</div>

<div class="mt-4 border-0 border-t border-solid border-surface-4 pt-4">
<DistributeBreakdownRow label="Net Actual Revenue" :value="netActualLabel" strong />
</div>

<div class="mt-4 border-0 border-t border-solid border-surface-4 pt-4">
<DistributeBreakdownRow label="Creator Revenue (75%)" :value="creatorRevenueLabel" />
<DistributeBreakdownRow label="Modrinth Revenue (25%)" :value="modrinthRevenueLabel" />
</div>
</div>
</template>

<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { computed } from 'vue'

import {
formatCurrency,
formatSignedCurrency,
getCreatorShare,
getModrinthShare,
getNetActualRevenue,
getTotalAdjustments,
type DistributionAdjustment,
} from '../utils'
import DistributeBreakdownRow from './DistributeBreakdownRow.vue'

const props = defineProps<{
payout: Labrinth.Payouts.Internal.HistoryItem
amountReceived: number | undefined
adjustments: DistributionAdjustment[]
}>()

const emptyValue = '—'

const estimatedRevenue = computed(() =>
props.payout.days.reduce((total, day) => total + (day.estimated_revenue_usd ?? 0), 0),
)
const hasActualAmount = computed(() => (props.amountReceived ?? 0) > 0)
const actualRevenue = computed(() => props.amountReceived ?? 0)
const totalAdjustments = computed(() => getTotalAdjustments(props.adjustments))
const netActualRevenue = computed(() => getNetActualRevenue(actualRevenue.value, props.adjustments))
const actualRevenueLabel = computed(() =>
hasActualAmount.value ? formatCurrency(actualRevenue.value, { cents: true }) : emptyValue,
)
const varianceResolutionLabel = computed(() =>
hasActualAmount.value ? formatSignedCurrency(totalAdjustments.value) : emptyValue,
)
const netActualLabel = computed(() =>
hasActualAmount.value ? formatCurrency(netActualRevenue.value, { cents: true }) : emptyValue,
)
const creatorRevenueLabel = computed(() =>
hasActualAmount.value
? formatCurrency(getCreatorShare(netActualRevenue.value), { cents: true })
: emptyValue,
)
const modrinthRevenueLabel = computed(() =>
hasActualAmount.value
? formatCurrency(getModrinthShare(netActualRevenue.value), { cents: true })
: emptyValue,
)
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<div class="mb-4 flex items-center justify-between gap-6 last:mb-0">
<span
class="font-base text-base"
:class="strong ? 'font-medium text-contrast' : 'text-primary'"
>
{{ label }}
</span>
<span
class="font-base text-right text-base"
:class="[
value === emptyValue
? 'text-primary opacity-60'
: strong
? 'text-lg font-medium text-contrast'
: 'text-primary',
negative ? 'text-red' : '',
]"
>
{{ value }}
</span>
</div>
</template>

<script setup lang="ts">
defineProps<{
label: string
value: string
negative?: boolean
strong?: boolean
}>()

const emptyValue = '—'
</script>
Loading