From 2c6c150ea324a3fbcdd3a5bf1ef08e0be0af1155 Mon Sep 17 00:00:00 2001
From: BlertaGalani <98878525+BlertaGalani@users.noreply.github.com>
Date: Mon, 27 Jan 2025 17:23:56 +0100
Subject: [PATCH] Add Salary Payouts modal

---
 .../curator-actions/Signatories.svelte        |  19 +-
 .../operations/CreateSalaryPayouts.svelte     | 239 ++++++++++++++++++
 2 files changed, 257 insertions(+), 1 deletion(-)
 create mode 100644 src/components/curator-actions/operations/CreateSalaryPayouts.svelte

diff --git a/src/components/curator-actions/Signatories.svelte b/src/components/curator-actions/Signatories.svelte
index f3300ba..f05030c 100644
--- a/src/components/curator-actions/Signatories.svelte
+++ b/src/components/curator-actions/Signatories.svelte
@@ -4,12 +4,16 @@
 	import { fetchMultisigSignatories } from './fetch-signatories';
 	import CopyableAddress from '../common/CopyableAddress.svelte';
 	import Dialog from '../common/Dialog.svelte';
+	import CreateSalaryPayouts from './operations/CreateSalaryPayouts.svelte';
+	import type { ChildBounty } from '../../types/child-bounty';
 
 	export let open = false;
 	export let bounty: Bounty;
+	export let childBounty: ChildBounty;
 	export let curatorAddress = '';
 
 	let signatories: string[] | undefined;
+	let salariesDialogOpen = false;
 
 	onMount(async () => {
 		if (curatorAddress) {
@@ -22,7 +26,7 @@
 
 <Dialog bind:open title="CURATORS LIST">
 	<div class="modal mt-5">
-		<div class="space-y-3">
+		<div class="space-y-5">
 			<div class="space-y-1">
 				<p class="font-bold">Curator Proxy:</p>
 				<p><CopyableAddress address={bounty.curator} /></p>
@@ -41,6 +45,19 @@
 					<p>No signatories found.</p>
 				{/if}
 			</section>
+			<button
+				on:click={() => {
+					salariesDialogOpen = true;
+				}}
+				class="w-full md:w-fit h-12 button-popup">CREATE SALARIES</button
+			>
 		</div>
 	</div>
 </Dialog>
+
+<CreateSalaryPayouts
+	{bounty}
+	bind:open={salariesDialogOpen}
+	curatorAddress={bounty.curator}
+	{childBounty}
+/>
diff --git a/src/components/curator-actions/operations/CreateSalaryPayouts.svelte b/src/components/curator-actions/operations/CreateSalaryPayouts.svelte
new file mode 100644
index 0000000..0c8c171
--- /dev/null
+++ b/src/components/curator-actions/operations/CreateSalaryPayouts.svelte
@@ -0,0 +1,239 @@
+<script lang="ts">
+	import { onMount } from 'svelte';
+	import CopyableAddress from '../../common/CopyableAddress.svelte';
+	import Dialog from '../../common/Dialog.svelte';
+	import { fetchMultisigSignatories } from '../fetch-signatories';
+	import { submitTransaction } from '../../../utils/transaction';
+	import { activeAccount, dotApi } from '../../../stores';
+	import { convertFormattedDotToPlanck, isValidAddress } from '../../../utils/polkadot';
+	import { isPositiveNumber } from '../../../utils/common';
+	import type { ChildBounty } from '../../../types/child-bounty';
+	import { MultiAddress } from '@polkadot-api/descriptors';
+	import { Binary } from 'polkadot-api';
+	import { showErrorDialog } from '../../../utils/loading-screen';
+	import type { Bounty } from '../../../types/bounty';
+
+	export let open = false;
+	export let curatorAddress = '';
+
+	let signatories: { address: string; salary: number | '' }[] = [];
+	let equalSalary: number | '' = '';
+	let totalSalary: number | '';
+	let isSalaryCustom = false;
+
+	onMount(async () => {
+		const fetchedSignatories = await fetchMultisigSignatories(curatorAddress);
+		signatories = fetchedSignatories.map((address) => ({ address, salary: '' }));
+	});
+
+	const applyEqualSalary = () => {
+		if (equalSalary !== '') {
+			isSalaryCustom = false;
+			signatories = signatories.map((s) => ({ ...s, salary: equalSalary }));
+			calculateTotalFromEqualSalaries();
+		}
+	};
+
+	const calculateTotalFromEqualSalaries = () => {
+		totalSalary = signatories.reduce((sum, s) => sum + (s.salary || 0), 0);
+	};
+
+	const calculateEqualSalariesFromTotal = () => {
+		if (totalSalary !== '' && signatories.length) {
+			equalSalary = Math.floor((totalSalary as number) / signatories.length);
+			signatories = signatories.map((s) => ({ ...s, salary: equalSalary }));
+		}
+	};
+
+	const handleSignatoryChange = (index: number, value: string) => {
+		const parsedValue = parseFloat(value);
+		signatories[index].salary = isNaN(parsedValue) ? '' : parsedValue;
+
+		const hasCustomSalaries = signatories.some((s) => s.salary !== equalSalary);
+		if (hasCustomSalaries) {
+			isSalaryCustom = true;
+			equalSalary = '';
+			totalSalary = '';
+		} else {
+			isSalaryCustom = false;
+			calculateTotalFromEqualSalaries();
+		}
+	};
+
+	$: if (isSalaryCustom) {
+		totalSalary = signatories.reduce((sum, s) => sum + (s.salary || 0), 0);
+	}
+
+	const getCurrentMonth = (): string => {
+		const date = new Date();
+		const monthNames = [
+			'January',
+			'February',
+			'March',
+			'April',
+			'May',
+			'June',
+			'July',
+			'August',
+			'September',
+			'October',
+			'November',
+			'December'
+		];
+		return monthNames[date.getMonth()];
+	};
+
+	let currentMonth: string = `${getCurrentMonth()} Salary`;
+
+	export let childBounty: ChildBounty;
+	export let bounty: Bounty;
+
+	let beneficiary = '';
+	let curatorFee = '';
+	let extend = false;
+	let nextAvailableChildBountyId: number;
+	let childBountyId: number;
+
+	(async () => {
+		nextAvailableChildBountyId = await $dotApi.query.ChildBounties.ChildBountyCount.getValue();
+		childBountyId = nextAvailableChildBountyId;
+	})();
+
+	$: transactions = signatories.map(({ address, salary }, childBountyId) => {
+		if (!$activeAccount || !isValidAddress(beneficiary) || !isPositiveNumber(curatorFee))
+			return null;
+
+		const create = $dotApi.tx.ChildBounties.add_child_bounty({
+			parent_bounty_id: bounty.id,
+			value: convertFormattedDotToPlanck(String(salary || 0)),
+			description: Binary.fromText(`${currentMonth} - ${childBountyId + 1}`)
+		});
+
+		const propose = $dotApi.tx.ChildBounties.propose_curator({
+			parent_bounty_id: childBounty.parentBounty,
+			child_bounty_id: childBounty.id + childBountyId,
+			curator: MultiAddress.Id($activeAccount.address),
+			fee: convertFormattedDotToPlanck(String(curatorFee))
+		});
+
+		const accept = $dotApi.tx.ChildBounties.accept_curator({
+			parent_bounty_id: childBounty.parentBounty,
+			child_bounty_id: childBounty.id + childBountyId
+		});
+
+		const award = $dotApi.tx.ChildBounties.award_child_bounty({
+			parent_bounty_id: childBounty.parentBounty,
+			child_bounty_id: childBounty.id + childBountyId,
+			beneficiary: MultiAddress.Id(address)
+		});
+
+		const claim = $dotApi.tx.ChildBounties.claim_child_bounty({
+			parent_bounty_id: childBounty.parentBounty,
+			child_bounty_id: childBounty.id + childBountyId
+		});
+
+		const extendTx = $dotApi.tx.Bounties.extend_bounty_expiry({
+			bounty_id: childBounty.parentBounty,
+			remark: new Binary(new Uint8Array())
+		});
+
+		return $dotApi.tx.Utility.batch_all({
+			calls: [
+				create.decodedCall,
+				propose.decodedCall,
+				accept.decodedCall,
+				award.decodedCall,
+				claim.decodedCall,
+				...(extend ? [extendTx.decodedCall] : [])
+			]
+		});
+	});
+
+	async function submit() {
+		open = false;
+
+		if (!$activeAccount) {
+			showErrorDialog('Wallet is not connected');
+			return;
+		}
+
+		if (!transactions.length) {
+			showErrorDialog('No transactions to submit.');
+			return;
+		}
+
+		for (const transaction of transactions) {
+			if (!transaction) continue;
+
+			try {
+				await submitTransaction(transaction);
+			} catch (error) {
+				showErrorDialog('Failed to process transaction for a signatory');
+				return;
+			}
+		}
+
+		showErrorDialog('All transactions have been processed successfully!');
+	}
+</script>
+
+<Dialog bind:open title="SALARY PAYOUTS" backgroundColor="white" textColor="primary">
+	<div class="mt-5 space-y-4">
+		<div>
+			<p class="text-xs">Edit the suggested title if desired</p>
+			<div class="flex items-baseline space-x-2">
+				<div class="my-1">
+					<input
+						class="border border-black pt-1 pl-2 rounded-[3px] bg-white h-10 text-primary"
+						bind:value={currentMonth}
+						placeholder={`${getCurrentMonth()} Salary`}
+					/>
+				</div>
+				<p>for Curator</p>
+			</div>
+		</div>
+
+		<div class="space-y-1">
+			<p class="text-xs">Enter an individual salary or the total payout</p>
+			<div class="flex justify-between items-baseline relative">
+				<input
+					class="border border-primary rounded-[3px] bg-white pl-2 pt-1 h-10 w-[30%] text-primary"
+					type="number"
+					bind:value={equalSalary}
+					placeholder="Individual salary"
+					on:input={applyEqualSalary}
+				/>
+				<p>per / signatory =</p>
+
+				<input
+					class="border border-primary rounded-[3px] bg-white pl-2 pt-1 h-10 w-[30%] text-primary"
+					type="number"
+					bind:value={totalSalary}
+					placeholder="Total salary"
+					on:input={calculateEqualSalariesFromTotal}
+				/>
+				<p>total</p>
+			</div>
+		</div>
+
+		<ul class="space-y-1.5">
+			{#each signatories as { address, salary }, index}
+				<li class="flex justify-between relative">
+					<CopyableAddress {address} />
+					<input
+						class="border border-primary rounded-[3px] bg-white pl-2 pt-1 h-10 text-primary"
+						type="number"
+						value={salary !== '' ? salary : ''}
+						placeholder="Enter salary"
+						on:input={(e) => handleSignatoryChange(index, e.currentTarget.value)}
+					/>
+				</li>
+			{/each}
+		</ul>
+
+		<p>Total: {totalSalary || 0}</p>
+		<button on:click={submit} class="w-full md:w-fit mt-10 h-12 bg-childBountyGray basic-button">
+			CREATE
+		</button>
+	</div>
+</Dialog>