You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
10 KiB
378 lines
10 KiB
/** * OfferDialog.vue - Dialog component for creating and submitting offers * *
|
|
Features: * - Offer creation with description and amount * - Unit code selection
|
|
(HUR, etc.) * - Expiration date handling * - Recipient and project targeting * -
|
|
Real-time validation and submission * - Comprehensive error handling and user
|
|
feedback * - Navigation to detailed offer configuration * * @author Matthew
|
|
Raymer */
|
|
<template>
|
|
<div v-if="visible" class="dialog-overlay">
|
|
<div class="dialog">
|
|
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
|
|
<input
|
|
v-model="description"
|
|
type="text"
|
|
data-testId="inputDescription"
|
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
|
placeholder="Description of what is offered"
|
|
/>
|
|
<div class="flex flex-row mt-2">
|
|
<span :class="unitCodeDisplayClasses" @click="changeUnitCode()">
|
|
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
|
|
</span>
|
|
<div
|
|
v-if="showDecrementButton"
|
|
:class="controlButtonClasses"
|
|
@click="decrement()"
|
|
>
|
|
<font-awesome icon="chevron-left" />
|
|
</div>
|
|
<input
|
|
v-model="amountInput"
|
|
data-testId="inputOfferAmount"
|
|
type="number"
|
|
:class="amountInputClasses"
|
|
/>
|
|
<div :class="incrementButtonClasses" @click="increment()">
|
|
<font-awesome icon="chevron-right" />
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 flex justify-center">
|
|
<span>
|
|
<router-link :to="offerDetailsRoute" class="text-blue-500">
|
|
Conditions & more options...
|
|
</router-link>
|
|
</span>
|
|
</div>
|
|
<p class="text-center mt-6 mb-2 italic">
|
|
Sign & Send to publish to the world
|
|
</p>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
<button :class="primaryButtonClasses" @click="confirm">
|
|
Sign & Send
|
|
</button>
|
|
<button :class="secondaryButtonClasses" @click="cancel">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
|
|
|
import { NotificationIface } from "../constants/app";
|
|
import { createAndSubmitOffer } from "../libs/endorserServer";
|
|
import * as libsUtil from "../libs/util";
|
|
import { logger } from "../utils/logger";
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|
import {
|
|
NOTIFY_OFFER_SETTINGS_ERROR,
|
|
NOTIFY_OFFER_RECORDING,
|
|
NOTIFY_OFFER_IDENTITY_REQUIRED,
|
|
NOTIFY_OFFER_DESCRIPTION_REQUIRED,
|
|
NOTIFY_OFFER_CREATION_ERROR,
|
|
NOTIFY_OFFER_SUCCESS,
|
|
NOTIFY_OFFER_SUBMISSION_ERROR,
|
|
} from "@/constants/notifications";
|
|
|
|
@Component({
|
|
mixins: [PlatformServiceMixin],
|
|
})
|
|
export default class OfferDialog extends Vue {
|
|
@Prop projectId?: string;
|
|
@Prop projectName?: string;
|
|
|
|
// Vue notification system
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
// Notification system
|
|
notify!: ReturnType<typeof createNotifyHelpers>;
|
|
|
|
// Component state
|
|
activeDid = "";
|
|
apiServer = "";
|
|
amountInput = "0";
|
|
amountUnitCode = "HUR";
|
|
description = "";
|
|
expirationDateInput = "";
|
|
recipientDid? = "";
|
|
recipientName? = "";
|
|
visible = false;
|
|
|
|
libsUtil = libsUtil;
|
|
|
|
// =================================================
|
|
// COMPUTED PROPERTIES - Template Logic Streamlining
|
|
// =================================================
|
|
|
|
/**
|
|
* CSS classes for the primary action button (Sign & Send)
|
|
* Reduces template complexity for gradient button styling
|
|
*/
|
|
get primaryButtonClasses(): string {
|
|
return "block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md";
|
|
}
|
|
|
|
/**
|
|
* CSS classes for the secondary action button (Cancel)
|
|
* Reduces template complexity for gradient button styling
|
|
*/
|
|
get secondaryButtonClasses(): string {
|
|
return "block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md";
|
|
}
|
|
|
|
/**
|
|
* CSS classes for unit code selector and increment/decrement buttons
|
|
* Reduces template complexity for repeated border and styling patterns
|
|
*/
|
|
get controlButtonClasses(): string {
|
|
return "border border-r-0 border-slate-400 bg-slate-200 px-4 py-2";
|
|
}
|
|
|
|
/**
|
|
* CSS classes for unit code display span
|
|
* Reduces template complexity for unit code button styling
|
|
*/
|
|
get unitCodeDisplayClasses(): string {
|
|
return "rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2";
|
|
}
|
|
|
|
/**
|
|
* CSS classes for amount input field
|
|
* Reduces template complexity for input styling
|
|
*/
|
|
get amountInputClasses(): string {
|
|
return "w-full border border-r-0 border-slate-400 px-2 py-2 text-center";
|
|
}
|
|
|
|
/**
|
|
* CSS classes for the right-most increment button
|
|
* Reduces template complexity for border styling
|
|
*/
|
|
get incrementButtonClasses(): string {
|
|
return "rounded-r border border-slate-400 bg-slate-200 px-4 py-2";
|
|
}
|
|
|
|
/**
|
|
* Router configuration object for offer details navigation
|
|
* Consolidates complex query parameter object from template
|
|
*/
|
|
get offerDetailsRoute(): object {
|
|
return {
|
|
name: "offer-details",
|
|
query: {
|
|
amountInput: this.amountInput,
|
|
description: this.description,
|
|
offererDid: this.activeDid,
|
|
projectId: this.projectId,
|
|
projectName: this.projectName,
|
|
recipientDid: this.recipientDid,
|
|
recipientName: this.recipientName,
|
|
unitCode: this.amountUnitCode,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Whether the decrement button should be visible
|
|
* Encapsulates conditional logic from template
|
|
*/
|
|
get showDecrementButton(): boolean {
|
|
return this.amountInput !== "0";
|
|
}
|
|
|
|
// =================================================
|
|
// COMPONENT METHODS
|
|
// =================================================
|
|
|
|
/**
|
|
* Vue lifecycle hook - Initialize notification helpers
|
|
*/
|
|
mounted() {
|
|
this.notify = createNotifyHelpers(this.$notify);
|
|
}
|
|
|
|
/**
|
|
* Open the dialog and load account settings
|
|
* @param recipientDid - Optional recipient DID
|
|
* @param recipientName - Optional recipient name
|
|
*/
|
|
async open(recipientDid?: string, recipientName?: string) {
|
|
try {
|
|
this.recipientDid = recipientDid;
|
|
this.recipientName = recipientName;
|
|
|
|
const settings = await this.$accountSettings();
|
|
this.apiServer = settings.apiServer || "";
|
|
this.activeDid = settings.activeDid || "";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (err: any) {
|
|
logger.error("Error retrieving settings from database:", err);
|
|
this.notify.error(
|
|
err.message || NOTIFY_OFFER_SETTINGS_ERROR.message,
|
|
TIMEOUTS.MODAL,
|
|
);
|
|
}
|
|
|
|
this.visible = true;
|
|
}
|
|
|
|
/**
|
|
* Close the dialog without changing values
|
|
*/
|
|
close() {
|
|
// close the dialog but don't change values (since it might be submitting info)
|
|
this.visible = false;
|
|
}
|
|
|
|
/**
|
|
* Cycle through available unit codes
|
|
*/
|
|
changeUnitCode() {
|
|
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
|
const index = units.indexOf(this.amountUnitCode);
|
|
this.amountUnitCode = units[(index + 1) % units.length];
|
|
}
|
|
|
|
/**
|
|
* Increment the amount input
|
|
*/
|
|
increment() {
|
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
|
}
|
|
|
|
/**
|
|
* Decrement the amount input
|
|
*/
|
|
decrement() {
|
|
this.amountInput = `${Math.max(
|
|
0,
|
|
(parseFloat(this.amountInput) || 1) - 1,
|
|
)}`;
|
|
}
|
|
|
|
/**
|
|
* Cancel the dialog and clear values
|
|
*/
|
|
cancel() {
|
|
this.close();
|
|
this.eraseValues();
|
|
}
|
|
|
|
/**
|
|
* Clear form values
|
|
*/
|
|
eraseValues() {
|
|
this.description = "";
|
|
this.amountInput = "0";
|
|
this.amountUnitCode = "HUR";
|
|
}
|
|
|
|
/**
|
|
* Confirm and submit the offer
|
|
*/
|
|
async confirm() {
|
|
this.close();
|
|
this.notify.toast(NOTIFY_OFFER_RECORDING.text, undefined, TIMEOUTS.BRIEF);
|
|
|
|
// this is asynchronous, but we don't need to wait for it to complete
|
|
this.recordOffer(
|
|
this.description,
|
|
parseFloat(this.amountInput),
|
|
this.amountUnitCode,
|
|
this.expirationDateInput,
|
|
).then(() => {
|
|
this.description = "";
|
|
this.amountInput = "0";
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Record an offer with the given parameters
|
|
* @param description - Offer description (may be empty)
|
|
* @param amount - Offer amount (may be 0)
|
|
* @param unitCode - Unit code (defaults to "HUR")
|
|
* @param expirationDateInput - Optional expiration date
|
|
*/
|
|
public async recordOffer(
|
|
description: string,
|
|
amount: number,
|
|
unitCode: string = "HUR",
|
|
expirationDateInput?: string,
|
|
) {
|
|
if (!this.activeDid) {
|
|
this.notify.error(NOTIFY_OFFER_IDENTITY_REQUIRED.message, TIMEOUTS.LONG);
|
|
return;
|
|
}
|
|
|
|
if (!description && !amount) {
|
|
const message = NOTIFY_OFFER_DESCRIPTION_REQUIRED.message.replace(
|
|
"{unit}",
|
|
this.libsUtil.UNIT_LONG[unitCode],
|
|
);
|
|
this.notify.error(message, TIMEOUTS.MODAL);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await createAndSubmitOffer(
|
|
this.axios,
|
|
this.apiServer,
|
|
this.activeDid,
|
|
description,
|
|
amount,
|
|
unitCode,
|
|
"",
|
|
expirationDateInput,
|
|
this.recipientDid,
|
|
this.projectId,
|
|
);
|
|
|
|
if (!result.success) {
|
|
const errorMessage = result.error;
|
|
logger.error("Error with offer creation result:", result);
|
|
this.notify.error(
|
|
errorMessage || NOTIFY_OFFER_CREATION_ERROR.message,
|
|
TIMEOUTS.MODAL,
|
|
);
|
|
} else {
|
|
this.notify.success(NOTIFY_OFFER_SUCCESS.message, TIMEOUTS.VERY_LONG);
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
logger.error("Error with offer recordation caught:", error);
|
|
const message =
|
|
error.userMessage ||
|
|
error.response?.data?.error?.message ||
|
|
NOTIFY_OFFER_SUBMISSION_ERROR.message;
|
|
this.notify.error(message, TIMEOUTS.MODAL);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.dialog-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.dialog {
|
|
background: white;
|
|
padding: 1.5rem;
|
|
border-radius: 0.5rem;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
}
|
|
</style>
|
|
|