forked from trent_larson/crowd-funder-for-time-pwa
- Restore runMigrations functionality for database schema migrations - Remove indexedDBMigrationService.ts (was for IndexedDB to SQLite migration) - Recreate migrationService.ts and db-sql/migration.ts for schema management - Add proper TypeScript error handling with type guards in AccountViewView - Fix CreateAndSubmitClaimResult property access in QuickActionBvcBeginView - Remove LeafletMouseEvent from Vue components array (it's a type, not component) - Add null check for UserNameDialog callback to prevent undefined assignment - Implement extractErrorMessage helper function for consistent error handling - Update router to remove database-migration route The migration system now properly handles database schema evolution across app versions, while the IndexedDB to SQLite migration service has been removed as it was specific to that one-time migration.
313 lines
7.9 KiB
Vue
313 lines
7.9 KiB
Vue
<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="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2"
|
|
@click="changeUnitCode()"
|
|
>
|
|
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
|
|
</span>
|
|
<div
|
|
v-if="amountInput !== '0'"
|
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
|
@click="decrement()"
|
|
>
|
|
<font-awesome icon="chevron-left" />
|
|
</div>
|
|
<input
|
|
v-model="amountInput"
|
|
data-testId="inputOfferAmount"
|
|
type="number"
|
|
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
|
/>
|
|
<div
|
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
|
@click="increment()"
|
|
>
|
|
<font-awesome icon="chevron-right" />
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 flex justify-center">
|
|
<span>
|
|
<router-link
|
|
:to="{
|
|
name: 'offer-details',
|
|
query: {
|
|
amountInput,
|
|
description,
|
|
offererDid: activeDid,
|
|
projectId,
|
|
projectName,
|
|
recipientDid,
|
|
recipientName,
|
|
unitCode: amountUnitCode,
|
|
},
|
|
}"
|
|
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="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"
|
|
@click="confirm"
|
|
>
|
|
Sign & Send
|
|
</button>
|
|
<button
|
|
class="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"
|
|
@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 * as databaseUtil from "../db/databaseUtil";
|
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
|
import { logger } from "../utils/logger";
|
|
|
|
@Component
|
|
export default class OfferDialog extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
@Prop projectId?: string;
|
|
@Prop projectName?: string;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
|
|
amountInput = "0";
|
|
amountUnitCode = "HUR";
|
|
description = "";
|
|
expirationDateInput = "";
|
|
recipientDid? = "";
|
|
recipientName? = "";
|
|
visible = false;
|
|
|
|
libsUtil = libsUtil;
|
|
|
|
async open(recipientDid?: string, recipientName?: string) {
|
|
try {
|
|
this.recipientDid = recipientDid;
|
|
this.recipientName = recipientName;
|
|
|
|
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
|
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(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: err.message || "There was an error retrieving your settings.",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
this.visible = true;
|
|
}
|
|
|
|
close() {
|
|
// close the dialog but don't change values (since it might be submitting info)
|
|
this.visible = false;
|
|
}
|
|
|
|
changeUnitCode() {
|
|
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
|
const index = units.indexOf(this.amountUnitCode);
|
|
this.amountUnitCode = units[(index + 1) % units.length];
|
|
}
|
|
|
|
increment() {
|
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
|
}
|
|
|
|
decrement() {
|
|
this.amountInput = `${Math.max(
|
|
0,
|
|
(parseFloat(this.amountInput) || 1) - 1,
|
|
)}`;
|
|
}
|
|
|
|
cancel() {
|
|
this.close();
|
|
this.eraseValues();
|
|
}
|
|
|
|
eraseValues() {
|
|
this.description = "";
|
|
this.amountInput = "0";
|
|
this.amountUnitCode = "HUR";
|
|
}
|
|
|
|
async confirm() {
|
|
this.close();
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "toast",
|
|
text: "Recording the offer...",
|
|
title: "",
|
|
},
|
|
1000,
|
|
);
|
|
// 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";
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param description may be an empty string
|
|
* @param hours may be 0
|
|
* @param unitCode may be omitted, defaults to "HUR"
|
|
*/
|
|
public async recordOffer(
|
|
description: string,
|
|
amount: number,
|
|
unitCode: string = "HUR",
|
|
expirationDateInput?: string,
|
|
) {
|
|
if (!this.activeDid) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "You must select an identity before you can record an offer.",
|
|
},
|
|
7000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!description && !amount) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`,
|
|
},
|
|
-1,
|
|
);
|
|
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(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: errorMessage || "There was an error creating the offer.",
|
|
},
|
|
-1,
|
|
);
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Success",
|
|
text: "That offer was recorded.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
// 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 ||
|
|
"There was an error recording the offer.";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: message,
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.dialog-overlay {
|
|
z-index: 50;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.dialog {
|
|
background-color: white;
|
|
padding: 1rem;
|
|
border-radius: 0.5rem;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
}
|
|
</style>
|