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.
 
 
 
 
 
 

339 lines
9.3 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 mb-4">
<AmountInput
:value="parseFloat(amountInput) || 0"
:on-update-value="handleAmountUpdate"
data-testId="inputOfferAmount"
/>
<select
v-model="amountUnitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
>
<option
v-for="(displayName, code) in unitOptions"
:key="code"
:value="code"
>
{{ displayName }}
</option>
</select>
</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 &amp; 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,
NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT,
} from "@/constants/notifications";
import AmountInput from "./AmountInput.vue";
@Component({
mixins: [PlatformServiceMixin],
components: {
AmountInput,
},
})
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";
}
/**
* Computed property to get unit options for the select dropdown
*/
get unitOptions() {
return this.libsUtil.UNIT_SHORT;
}
/**
* 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,
},
};
}
// =================================================
// 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;
}
/**
* Handle amount updates from AmountInput component
* @param value - New amount value
*/
handleAmountUpdate(value: number) {
this.amountInput = value.toString();
}
/**
* 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() {
if (!this.activeDid) {
this.notify.error(NOTIFY_OFFER_IDENTITY_REQUIRED.message, TIMEOUTS.LONG);
return;
}
if (parseFloat(this.amountInput) < 0) {
this.notify.error(
NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT.message,
TIMEOUTS.SHORT,
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
const message = NOTIFY_OFFER_DESCRIPTION_REQUIRED.message.replace(
"{unit}",
this.libsUtil.UNIT_LONG[this.amountUnitCode],
);
this.notify.error(message, TIMEOUTS.SHORT);
return;
}
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,
) {
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: 50;
}
.dialog {
background: white;
padding: 1.5rem;
border-radius: 0.5rem;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
</style>