Compare commits

...
Author SHA1 Message Date
jose 030960dd59 Merge branch 'master' into claimview-fullfills-offer 2025-09-08 04:36:48 -04:00
trentlarson b138441d10 chore: change logging level to debug for debug messages 2025-09-07 18:34:57 -06:00
Jose Olarte III 1eeb013638 refactor(claims): extract offer fulfillment logic to utility function
Created extractOfferFulfillment utility in libs/util.ts to handle both
array and single object cases for fulfills field. Updated ClaimView and
ConfirmGiftView to use the shared utility, eliminating code duplication
and improving maintainability.
2025-09-01 21:24:46 +08:00
Jose Olarte III 3e5e2cd0bb fix(claims): handle single Offer object in fulfills field for ConfirmGiftView
Updated extractOfferFulfillment to support both array and single object
cases for the fulfills field, matching the fix applied to ClaimView.
Now handles when fulfills contains a single Offer object with @type "Offer".
2025-09-01 21:18:08 +08:00
Jose Olarte III d87f44b75d fix(claims): handle single Offer object in fulfills field
Updated extractOfferFulfillment to support both array and single object
cases for the fulfills field. Previously only handled array format,
now also checks if fulfills is a single Offer object with @type "Offer".
2025-09-01 21:06:48 +08:00
Jose Olarte III e5ad71505c Chore: move function to serverUtil
- capitalizeAndInsertSpacesBeforeCapsWithAPrefix() defined in two places, unified and moved to endorserServer.ts
- Use capitalizeAndInsertSpacesBeforeCaps() that's already defined in endorserServer.ts
2025-08-18 17:47:33 +08:00
trentlarson 19f0c270d3 chore: Rename variable for clarity 2025-08-17 14:13:50 -06:00
Jose Olarte III 693173f09d UI: wording and spacing consistencies
- Added grouped conditional spacing to ensure a top margin before fulfills links
- Brought over icons and wording from ConfirmGiftView to ClaimView
2025-08-14 20:12:28 +08:00
Jose Olarte III a1388539c1 Fix: improve offer fulfillment detection in ClaimView
- Remove outdated fulfillsType logic that was checking for non-PlanAction items
- Keep only the new offer fulfillment extraction from fullClaim.fulfills array
- Apply consistent changes to both ClaimView and ConfirmGiftView

This ensures that "Fulfills Offer..." links appear correctly when gives are created from offers, by directly parsing the fulfills array instead of relying on API-processed fields that only capture the first relationship.
2025-08-14 18:53:12 +08:00
5 changed files with 257 additions and 179 deletions
+22
View File
@@ -1313,6 +1313,28 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1"); : text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}; };
/**
* Formats type string for display by adding spaces before capitals
* and optionally adds an appropriate article prefix (a/an)
*
* @param text - Text to format
* @returns Formatted string with article prefix
*/
export const capitalizeAndInsertSpacesBeforeCapsWithAPrefix = (
text: string,
): string => {
const word = capitalizeAndInsertSpacesBeforeCaps(text);
if (word) {
// if the word starts with a vowel, use "an" instead of "a"
const firstLetter = word[0].toLowerCase();
const vowels = ["a", "e", "i", "o", "u"];
const particle = vowels.includes(firstLetter) ? "an" : "a";
return particle + " " + word;
} else {
return "";
}
};
/** /**
return readable summary of claim, or something generic return readable summary of claim, or something generic
+35
View File
@@ -160,6 +160,41 @@ export const isGiveAction = (
return isGiveClaimType(veriClaim.claimType); return isGiveClaimType(veriClaim.claimType);
}; };
export interface OfferFulfillment {
offerHandleId: string;
offerType: string;
}
/**
* Extract offer fulfillment information from the fulfills field
* Handles both array and single object cases
*/
export const extractOfferFulfillment = (fulfills: any): OfferFulfillment | null => {
if (!fulfills) {
return null;
}
// Handle both array and single object cases
let offerFulfill = null;
if (Array.isArray(fulfills)) {
// Find the Offer in the fulfills array
offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
} else if (fulfills["@type"] === "Offer") {
// fulfills is a single Offer object
offerFulfill = fulfills;
}
if (offerFulfill) {
return {
offerHandleId: offerFulfill.identifier,
offerType: offerFulfill["@type"],
};
}
return null;
};
export const shortDid = (did: string) => { export const shortDid = (did: string) => {
if (did.startsWith("did:peer:")) { if (did.startsWith("did:peer:")) {
return ( return (
+32 -32
View File
@@ -36,7 +36,7 @@ export class WebInlineQRScanner implements QRScannerService {
// Generate a short random ID for this scanner instance // Generate a short random ID for this scanner instance
this.id = Math.random().toString(36).substring(2, 8).toUpperCase(); this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
this.options = options ?? {}; this.options = options ?? {};
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`, `[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
{ {
...this.options, ...this.options,
@@ -49,7 +49,7 @@ export class WebInlineQRScanner implements QRScannerService {
this.context = this.canvas.getContext("2d", { willReadFrequently: true }); this.context = this.canvas.getContext("2d", { willReadFrequently: true });
this.video = document.createElement("video"); this.video = document.createElement("video");
this.video.setAttribute("playsinline", "true"); // Required for iOS this.video.setAttribute("playsinline", "true"); // Required for iOS
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] DOM elements created successfully`, `[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
); );
} }
@@ -60,7 +60,7 @@ export class WebInlineQRScanner implements QRScannerService {
this.cameraStateListeners.forEach((listener) => { this.cameraStateListeners.forEach((listener) => {
try { try {
listener.onStateChange(state, message); listener.onStateChange(state, message);
logger.info( logger.debug(
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`, `[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
{ {
state, state,
@@ -89,7 +89,7 @@ export class WebInlineQRScanner implements QRScannerService {
async checkPermissions(): Promise<boolean> { async checkPermissions(): Promise<boolean> {
try { try {
this.updateCameraState("initializing", "Checking camera permissions..."); this.updateCameraState("initializing", "Checking camera permissions...");
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`, `[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
); );
@@ -99,7 +99,7 @@ export class WebInlineQRScanner implements QRScannerService {
const permissions = await navigator.permissions.query({ const permissions = await navigator.permissions.query({
name: "camera" as PermissionName, name: "camera" as PermissionName,
}); });
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`, `[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
permissions.state, permissions.state,
); );
@@ -165,7 +165,7 @@ export class WebInlineQRScanner implements QRScannerService {
"initializing", "initializing",
"Requesting camera permissions...", "Requesting camera permissions...",
); );
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`, `[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
); );
@@ -175,7 +175,7 @@ export class WebInlineQRScanner implements QRScannerService {
(device) => device.kind === "videoinput", (device) => device.kind === "videoinput",
); );
logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, { logger.debug(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
count: videoDevices.length, count: videoDevices.length,
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })), devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
userAgent: navigator.userAgent, userAgent: navigator.userAgent,
@@ -188,7 +188,7 @@ export class WebInlineQRScanner implements QRScannerService {
} }
// Try to get a stream with specific constraints // Try to get a stream with specific constraints
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`, `[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
{ {
facingMode: "environment", facingMode: "environment",
@@ -210,7 +210,7 @@ export class WebInlineQRScanner implements QRScannerService {
// Stop the test stream immediately // Stop the test stream immediately
stream.getTracks().forEach((track) => { stream.getTracks().forEach((track) => {
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, { logger.debug(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
kind: track.kind, kind: track.kind,
label: track.label, label: track.label,
readyState: track.readyState, readyState: track.readyState,
@@ -275,12 +275,12 @@ export class WebInlineQRScanner implements QRScannerService {
async isSupported(): Promise<boolean> { async isSupported(): Promise<boolean> {
try { try {
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Checking browser support...`, `[WebInlineQRScanner:${this.id}] Checking browser support...`,
); );
// Check for secure context first // Check for secure context first
if (!window.isSecureContext) { if (!window.isSecureContext) {
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`, `[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
); );
return false; return false;
@@ -300,7 +300,7 @@ export class WebInlineQRScanner implements QRScannerService {
(device) => device.kind === "videoinput", (device) => device.kind === "videoinput",
); );
logger.error(`[WebInlineQRScanner:${this.id}] Device support check:`, { logger.debug(`[WebInlineQRScanner:${this.id}] Device support check:`, {
hasSecureContext: window.isSecureContext, hasSecureContext: window.isSecureContext,
hasMediaDevices: !!navigator.mediaDevices, hasMediaDevices: !!navigator.mediaDevices,
hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia, hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
@@ -379,7 +379,7 @@ export class WebInlineQRScanner implements QRScannerService {
// Log scan attempt every 100 frames or 1 second // Log scan attempt every 100 frames or 1 second
if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) { if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
logger.error(`[WebInlineQRScanner:${this.id}] Scanning frame:`, { logger.debug(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
attempt: this.scanAttempts, attempt: this.scanAttempts,
dimensions: { dimensions: {
width: this.canvas.width, width: this.canvas.width,
@@ -421,7 +421,7 @@ export class WebInlineQRScanner implements QRScannerService {
!code.data || !code.data ||
code.data.length === 0; code.data.length === 0;
logger.error(`[WebInlineQRScanner:${this.id}] QR Code detected:`, { logger.debug(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
data: code.data, data: code.data,
location: code.location, location: code.location,
attempts: this.scanAttempts, attempts: this.scanAttempts,
@@ -512,13 +512,13 @@ export class WebInlineQRScanner implements QRScannerService {
this.scanAttempts = 0; this.scanAttempts = 0;
this.lastScanTime = Date.now(); this.lastScanTime = Date.now();
this.updateCameraState("initializing", "Starting camera..."); this.updateCameraState("initializing", "Starting camera...");
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Starting scan with options:`, `[WebInlineQRScanner:${this.id}] Starting scan with options:`,
this.options, this.options,
); );
// Get camera stream with options // Get camera stream with options
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`, `[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
); );
this.stream = await navigator.mediaDevices.getUserMedia({ this.stream = await navigator.mediaDevices.getUserMedia({
@@ -531,7 +531,7 @@ export class WebInlineQRScanner implements QRScannerService {
this.updateCameraState("active", "Camera is active"); this.updateCameraState("active", "Camera is active");
logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, { logger.debug(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
tracks: this.stream.getTracks().map((t) => ({ tracks: this.stream.getTracks().map((t) => ({
kind: t.kind, kind: t.kind,
label: t.label, label: t.label,
@@ -550,14 +550,14 @@ export class WebInlineQRScanner implements QRScannerService {
this.video.style.display = "none"; this.video.style.display = "none";
} }
await this.video.play(); await this.video.play();
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Video element started playing`, `[WebInlineQRScanner:${this.id}] Video element started playing`,
); );
} }
// Emit stream to component // Emit stream to component
this.events.emit("stream", this.stream); this.events.emit("stream", this.stream);
logger.error(`[WebInlineQRScanner:${this.id}] Stream event emitted`); logger.debug(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
// Start QR code scanning // Start QR code scanning
this.scanQRCode(); this.scanQRCode();
@@ -595,7 +595,7 @@ export class WebInlineQRScanner implements QRScannerService {
} }
try { try {
logger.error(`[WebInlineQRScanner:${this.id}] Stopping scan`, { logger.debug(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
scanAttempts: this.scanAttempts, scanAttempts: this.scanAttempts,
duration: Date.now() - this.lastScanTime, duration: Date.now() - this.lastScanTime,
}); });
@@ -604,7 +604,7 @@ export class WebInlineQRScanner implements QRScannerService {
if (this.animationFrameId !== null) { if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId); cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null; this.animationFrameId = null;
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Animation frame cancelled`, `[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
); );
} }
@@ -613,13 +613,13 @@ export class WebInlineQRScanner implements QRScannerService {
if (this.video) { if (this.video) {
this.video.pause(); this.video.pause();
this.video.srcObject = null; this.video.srcObject = null;
logger.error(`[WebInlineQRScanner:${this.id}] Video element stopped`); logger.debug(`[WebInlineQRScanner:${this.id}] Video element stopped`);
} }
// Stop all tracks in the stream // Stop all tracks in the stream
if (this.stream) { if (this.stream) {
this.stream.getTracks().forEach((track) => { this.stream.getTracks().forEach((track) => {
logger.error(`[WebInlineQRScanner:${this.id}] Stopping track:`, { logger.debug(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
kind: track.kind, kind: track.kind,
label: track.label, label: track.label,
readyState: track.readyState, readyState: track.readyState,
@@ -631,7 +631,7 @@ export class WebInlineQRScanner implements QRScannerService {
// Emit stream stopped event // Emit stream stopped event
this.events.emit("stream", null); this.events.emit("stream", null);
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`, `[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
); );
} catch (error) { } catch (error) {
@@ -643,17 +643,17 @@ export class WebInlineQRScanner implements QRScannerService {
throw error; throw error;
} finally { } finally {
this.isScanning = false; this.isScanning = false;
logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`); logger.debug(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
} }
} }
addListener(listener: ScanListener): void { addListener(listener: ScanListener): void {
logger.error(`[WebInlineQRScanner:${this.id}] Adding scan listener`); logger.debug(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
this.scanListener = listener; this.scanListener = listener;
} }
onStream(callback: (stream: MediaStream | null) => void): void { onStream(callback: (stream: MediaStream | null) => void): void {
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Adding stream event listener`, `[WebInlineQRScanner:${this.id}] Adding stream event listener`,
); );
this.events.on("stream", callback); this.events.on("stream", callback);
@@ -661,24 +661,24 @@ export class WebInlineQRScanner implements QRScannerService {
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
try { try {
logger.error(`[WebInlineQRScanner:${this.id}] Starting cleanup`); logger.debug(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
await this.stopScan(); await this.stopScan();
this.events.removeAllListeners(); this.events.removeAllListeners();
logger.error(`[WebInlineQRScanner:${this.id}] Event listeners removed`); logger.debug(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
// Clean up DOM elements // Clean up DOM elements
if (this.video) { if (this.video) {
this.video.remove(); this.video.remove();
this.video = null; this.video = null;
logger.error(`[WebInlineQRScanner:${this.id}] Video element removed`); logger.debug(`[WebInlineQRScanner:${this.id}] Video element removed`);
} }
if (this.canvas) { if (this.canvas) {
this.canvas.remove(); this.canvas.remove();
this.canvas = null; this.canvas = null;
logger.error(`[WebInlineQRScanner:${this.id}] Canvas element removed`); logger.debug(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
} }
this.context = null; this.context = null;
logger.error( logger.debug(
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`, `[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
); );
} catch (error) { } catch (error) {
+59 -27
View File
@@ -24,7 +24,9 @@
<div class="flex columns-3"> <div class="flex columns-3">
<h2 class="text-md font-bold w-full"> <h2 class="text-md font-bold w-full">
{{ {{
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "") serverUtil.capitalizeAndInsertSpacesBeforeCaps(
veriClaim.claimType || "",
)
}} }}
<button <button
v-if="canEditClaim" v-if="canEditClaim"
@@ -106,9 +108,9 @@
</div> </div>
<!-- Fullfills Links --> <!-- Fullfills Links -->
<div class="mt-4 empty:hidden">
<!-- fullfills links for a give --> <!-- fullfills links for a give -->
<div v-if="detailsForGive?.fulfillsPlanHandleId" class="mt-4"> <div v-if="detailsForGive?.fulfillsPlanHandleId">
<router-link <router-link
:to=" :to="
'/project/' + '/project/' +
@@ -116,30 +118,35 @@
" "
class="text-blue-500 mt-2" class="text-blue-500 mt-2"
> >
Fulfills a bigger plan... This fulfills a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link> </router-link>
</div> </div>
<!-- if there's another, it's probably fulfilling an offer, too -->
<div <!-- Show offer fulfillment if this give fulfills an offer -->
v-if=" <div v-if="detailsForGiveOfferFulfillment?.offerHandleId">
detailsForGive?.fulfillsType &&
detailsForGive?.fulfillsType !== 'PlanAction' &&
detailsForGive?.fulfillsHandleId
"
>
<!-- router-link to /claim/ only changes URL path --> <!-- router-link to /claim/ only changes URL path -->
<a <a
class="text-blue-500 mt-4 cursor-pointer" class="text-blue-500 mt-4 cursor-pointer"
@click=" @click="
showDifferentClaimPage(detailsForGive?.fulfillsHandleId) showDifferentClaimPage(
detailsForGiveOfferFulfillment.offerHandleId,
)
" "
> >
Fulfills This fulfills
{{ {{
capitalizeAndInsertSpacesBeforeCaps( serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
detailsForGive.fulfillsType, detailsForGiveOfferFulfillment.offerType || "Offer",
) )
}}... }}
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a> </a>
</div> </div>
@@ -152,12 +159,16 @@
" "
class="text-blue-500 mt-4" class="text-blue-500 mt-4"
> >
Offered to a bigger plan... Offered to a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link> </router-link>
</div> </div>
<!-- Providers --> <!-- Providers -->
<div v-if="providersForGive?.length > 0" class="mt-4"> <div v-if="providersForGive?.length > 0">
<span>Other assistance provided by:</span> <span>Other assistance provided by:</span>
<ul class="ml-4"> <ul class="ml-4">
<li <li
@@ -171,7 +182,11 @@
class="text-blue-500 mt-4 cursor-pointer" class="text-blue-500 mt-4 cursor-pointer"
@click="handleProviderClick(provider)" @click="handleProviderClick(provider)"
> >
an activity... an activity
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a> </a>
</div> </div>
</div> </div>
@@ -182,6 +197,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="mt-2"> <div class="mt-2">
<font-awesome icon="comment" class="text-slate-400" /> <font-awesome icon="comment" class="text-slate-400" />
{{ issuerName }} posted that. {{ issuerName }} posted that.
@@ -556,6 +572,17 @@ export default class ClaimView extends Vue {
fulfillsPlanHandleId?: string; fulfillsPlanHandleId?: string;
fulfillsType?: string; fulfillsType?: string;
fulfillsHandleId?: string; fulfillsHandleId?: string;
fullClaim?: {
fulfills?: Array<{
"@type": string;
identifier?: string;
}>;
};
} | null = null;
// Additional offer information extracted from the fulfills array
detailsForGiveOfferFulfillment: {
offerHandleId?: string;
offerType?: string;
} | null = null; } | null = null;
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null; detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
// Project information for fulfillsPlanHandleId // Project information for fulfillsPlanHandleId
@@ -689,6 +716,7 @@ export default class ClaimView extends Vue {
this.confsVisibleToIdList = []; this.confsVisibleToIdList = [];
this.detailsForGive = null; this.detailsForGive = null;
this.detailsForOffer = null; this.detailsForOffer = null;
this.detailsForGiveOfferFulfillment = null;
this.projectInfo = null; this.projectInfo = null;
this.fullClaim = null; this.fullClaim = null;
this.fullClaimDump = ""; this.fullClaimDump = "";
@@ -701,6 +729,15 @@ export default class ClaimView extends Vue {
this.veriClaimDidsVisible = {}; this.veriClaimDidsVisible = {};
} }
/**
* Extract offer fulfillment information from the fulfills array
*/
extractOfferFulfillment() {
this.detailsForGiveOfferFulfillment = libsUtil.extractOfferFulfillment(
this.detailsForGive?.fullClaim?.fulfills
);
}
// ================================================= // =================================================
// UTILITY METHODS // UTILITY METHODS
// ================================================= // =================================================
@@ -758,13 +795,6 @@ export default class ClaimView extends Vue {
this.canShare = !!navigator.share; this.canShare = !!navigator.share;
} }
// insert a space before any capital letters except the initial letter
// (and capitalize initial letter, just in case)
capitalizeAndInsertSpacesBeforeCaps(text: string): string {
if (!text) return "";
return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}
totalConfirmers() { totalConfirmers() {
return ( return (
this.numConfsNotVisible + this.numConfsNotVisible +
@@ -821,6 +851,8 @@ export default class ClaimView extends Vue {
}); });
if (giveResp.status === 200 && giveResp.data.data?.length > 0) { if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
this.detailsForGive = giveResp.data.data[0]; this.detailsForGive = giveResp.data.data[0];
// Extract offer information from the fulfills array
this.extractOfferFulfillment();
} else { } else {
await this.$logError( await this.$logError(
"Error getting detailed give info: " + JSON.stringify(giveResp), "Error getting detailed give info: " + JSON.stringify(giveResp),
+30 -41
View File
@@ -96,13 +96,15 @@
</div> </div>
<!-- Fullfills Links --> <!-- Fullfills Links -->
<div class="mt-4">
<!-- fullfills links for a give --> <!-- fullfills links for a give -->
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2"> <div v-if="giveDetails?.fulfillsPlanHandleId">
<router-link <router-link
:to=" :to="
'/project/' + '/project/' +
encodeURIComponent(giveDetails?.fulfillsPlanHandleId || '') encodeURIComponent(
giveDetails?.fulfillsPlanHandleId || '',
)
" "
class="text-blue-500 mt-2 cursor-pointer" class="text-blue-500 mt-2 cursor-pointer"
> >
@@ -113,26 +115,23 @@
/> />
</router-link> </router-link>
</div> </div>
<!-- if there's another, it's probably fulfilling an offer, too -->
<div <!-- Show offer fulfillment if this give fulfills an offer -->
v-if=" <div v-if="giveDetailsOfferFulfillment?.offerHandleId">
giveDetails?.fulfillsType &&
giveDetails?.fulfillsType !== 'PlanAction' &&
giveDetails?.fulfillsHandleId
"
>
<!-- router-link to /claim/ only changes URL path --> <!-- router-link to /claim/ only changes URL path -->
<router-link <router-link
:to=" :to="
'/claim/' + '/claim/' +
encodeURIComponent(giveDetails?.fulfillsHandleId || '') encodeURIComponent(
giveDetailsOfferFulfillment.offerHandleId || '',
)
" "
class="text-blue-500 mt-2 cursor-pointer" class="text-blue-500 mt-2 cursor-pointer"
> >
This fulfills This fulfills
{{ {{
capitalizeAndInsertSpacesBeforeCapsWithAPrefix( serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
giveDetails?.fulfillsType || "", giveDetailsOfferFulfillment.offerType || "Offer",
) )
}} }}
<font-awesome <font-awesome
@@ -145,6 +144,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="mt-2"> <div class="mt-2">
<font-awesome icon="comment" class="text-slate-400" /> <font-awesome icon="comment" class="text-slate-400" />
{{ issuerName }} posted that. {{ issuerName }} posted that.
@@ -493,6 +493,11 @@ export default class ConfirmGiftView extends Vue {
confsVisibleErrorMessage = ""; confsVisibleErrorMessage = "";
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
giveDetails?: GiveSummaryRecord; giveDetails?: GiveSummaryRecord;
// Additional offer information extracted from the fulfills array
giveDetailsOfferFulfillment: {
offerHandleId?: string;
offerType?: string;
} | null = null;
giverName = ""; giverName = "";
issuerName = ""; issuerName = "";
isLoading = false; isLoading = false;
@@ -648,6 +653,8 @@ export default class ConfirmGiftView extends Vue {
if (resp.status === 200) { if (resp.status === 200) {
this.giveDetails = resp.data.data[0]; this.giveDetails = resp.data.data[0];
// Extract offer information from the fulfills array
this.extractOfferFulfillment();
} else { } else {
throw new Error("Error getting detailed give info: " + resp.status); throw new Error("Error getting detailed give info: " + resp.status);
} }
@@ -707,6 +714,15 @@ export default class ConfirmGiftView extends Vue {
} }
} }
/**
* Extract offer fulfillment information from the fulfills array
*/
private extractOfferFulfillment() {
this.giveDetailsOfferFulfillment = libsUtil.extractOfferFulfillment(
this.giveDetails?.fullClaim?.fulfills
);
}
/** /**
* Fetches confirmer information for the claim * Fetches confirmer information for the claim
*/ */
@@ -849,27 +865,6 @@ export default class ConfirmGiftView extends Vue {
); );
} }
/**
* Formats type string for display by adding spaces before capitals
* Optionally adds a prefix
*
* @param text - Text to format
* @param prefix - Optional prefix to add
* @returns Formatted string
*/
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string): string {
const word = this.capitalizeAndInsertSpacesBeforeCaps(text);
if (word) {
// if the word starts with a vowel, use "an" instead of "a"
const firstLetter = word[0].toLowerCase();
const vowels = ["a", "e", "i", "o", "u"];
const particle = vowels.includes(firstLetter) ? "an" : "a";
return particle + " " + word;
} else {
return "";
}
}
/** /**
* Initiates sharing of claim information * Initiates sharing of claim information
* Handles share functionality based on platform capabilities * Handles share functionality based on platform capabilities
@@ -894,11 +889,5 @@ export default class ConfirmGiftView extends Vue {
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
this.veriClaimDump = ""; this.veriClaimDump = "";
} }
capitalizeAndInsertSpacesBeforeCaps(text: string) {
return !text
? ""
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
}
} }
</script> </script>