|
@ -48,7 +48,7 @@ |
|
|
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts) |
|
|
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts) |
|
|
}}</span> |
|
|
}}</span> |
|
|
offered |
|
|
offered |
|
|
<span v-if="offer.objectDescription">{{ |
|
|
<span v-if="offer.objectDescription" class="truncate">{{ |
|
|
offer.objectDescription |
|
|
offer.objectDescription |
|
|
}}</span |
|
|
}}</span |
|
|
>{{ offer.objectDescription && offer.amount ? ", and " : "" }} |
|
|
>{{ offer.objectDescription && offer.amount ? ", and " : "" }} |
|
@ -70,7 +70,7 @@ |
|
|
@click="markOffersAsReadStartingWith(offer.jwtId)" |
|
|
@click="markOffersAsReadStartingWith(offer.jwtId)" |
|
|
> |
|
|
> |
|
|
<span class="inline-block w-8 h-px bg-gray-500 mr-2" /> |
|
|
<span class="inline-block w-8 h-px bg-gray-500 mr-2" /> |
|
|
Click to keep all above as new offers |
|
|
Click to keep all above as unread offers |
|
|
</div> |
|
|
</div> |
|
|
</li> |
|
|
</li> |
|
|
</ul> |
|
|
</ul> |
|
@ -115,7 +115,7 @@ |
|
|
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts) |
|
|
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts) |
|
|
}}</span> |
|
|
}}</span> |
|
|
offered |
|
|
offered |
|
|
<span v-if="offer.objectDescription">{{ |
|
|
<span v-if="offer.objectDescription" class="truncate">{{ |
|
|
offer.objectDescription |
|
|
offer.objectDescription |
|
|
}}</span |
|
|
}}</span |
|
|
>{{ offer.objectDescription && offer.amount ? ", and " : "" }} |
|
|
>{{ offer.objectDescription && offer.amount ? ", and " : "" }} |
|
@ -139,7 +139,7 @@ |
|
|
@click="markOffersToUserProjectsAsReadStartingWith(offer.jwtId)" |
|
|
@click="markOffersToUserProjectsAsReadStartingWith(offer.jwtId)" |
|
|
> |
|
|
> |
|
|
<span class="inline-block w-8 h-px bg-gray-500 mr-2" /> |
|
|
<span class="inline-block w-8 h-px bg-gray-500 mr-2" /> |
|
|
Click to keep all above as new offers |
|
|
Click to keep all above as unread offers |
|
|
</div> |
|
|
</div> |
|
|
</li> |
|
|
</li> |
|
|
</ul> |
|
|
</ul> |
|
@ -182,31 +182,90 @@ |
|
|
<span class="font-medium">{{ |
|
|
<span class="font-medium">{{ |
|
|
projectChange.plan.name || "Unnamed Project" |
|
|
projectChange.plan.name || "Unnamed Project" |
|
|
}}</span> |
|
|
}}</span> |
|
|
<span v-if="projectChange.plan.description" class="text-gray-600"> |
|
|
<span |
|
|
|
|
|
v-if="projectChange.plan.description" |
|
|
|
|
|
class="text-gray-600 truncate" |
|
|
|
|
|
> |
|
|
- {{ projectChange.plan.description }} |
|
|
- {{ projectChange.plan.description }} |
|
|
</span> |
|
|
</span> |
|
|
<router-link |
|
|
<router-link |
|
|
:to="{ |
|
|
:to="{ |
|
|
path: '/plan/' + encodeURIComponent(projectChange.plan.handleId), |
|
|
path: |
|
|
|
|
|
'/project/' + encodeURIComponent(projectChange.plan.handleId), |
|
|
}" |
|
|
}" |
|
|
class="text-blue-500" |
|
|
class="text-blue-500" |
|
|
> |
|
|
> |
|
|
<font-awesome |
|
|
<font-awesome |
|
|
icon="external-link-alt" |
|
|
icon="file-lines" |
|
|
class="pl-2 text-blue-500 cursor-pointer" |
|
|
class="pl-2 text-blue-500 cursor-pointer" |
|
|
/> |
|
|
/> |
|
|
</router-link> |
|
|
</router-link> |
|
|
|
|
|
<!-- Show what changed --> |
|
|
|
|
|
<div |
|
|
|
|
|
v-if="getPlanDifferences(projectChange.plan.handleId)" |
|
|
|
|
|
class="text-sm mt-2" |
|
|
|
|
|
> |
|
|
|
|
|
<div class="font-medium mb-2">Changes</div> |
|
|
|
|
|
<div class="overflow-x-auto"> |
|
|
|
|
|
<table |
|
|
|
|
|
class="w-full text-xs border-collapse border border-gray-300 rounded-lg shadow-sm bg-white" |
|
|
|
|
|
> |
|
|
|
|
|
<thead> |
|
|
|
|
|
<tr class="bg-gray-50"> |
|
|
|
|
|
<th |
|
|
|
|
|
class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700" |
|
|
|
|
|
></th> |
|
|
|
|
|
<th |
|
|
|
|
|
class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700" |
|
|
|
|
|
> |
|
|
|
|
|
Previous |
|
|
|
|
|
</th> |
|
|
|
|
|
<th |
|
|
|
|
|
class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700" |
|
|
|
|
|
> |
|
|
|
|
|
Current |
|
|
|
|
|
</th> |
|
|
|
|
|
</tr> |
|
|
|
|
|
</thead> |
|
|
|
|
|
<tbody> |
|
|
|
|
|
<tr |
|
|
|
|
|
v-for="(difference, field) in getPlanDifferences( |
|
|
|
|
|
projectChange.plan.handleId, |
|
|
|
|
|
)" |
|
|
|
|
|
:key="field" |
|
|
|
|
|
class="hover:bg-gray-50" |
|
|
|
|
|
> |
|
|
|
|
|
<td |
|
|
|
|
|
class="border border-gray-300 px-3 py-2 font-medium text-gray-800" |
|
|
|
|
|
> |
|
|
|
|
|
{{ getDisplayFieldName(field) }} |
|
|
|
|
|
</td> |
|
|
|
|
|
<td |
|
|
|
|
|
class="border border-gray-300 px-3 py-2 text-gray-600 break-words" |
|
|
|
|
|
> |
|
|
|
|
|
{{ formatFieldValue(difference.old) }} |
|
|
|
|
|
</td> |
|
|
|
|
|
<td |
|
|
|
|
|
class="border border-gray-300 px-3 py-2 text-green-700 font-medium break-words" |
|
|
|
|
|
> |
|
|
|
|
|
{{ formatFieldValue(difference.new) }} |
|
|
|
|
|
</td> |
|
|
|
|
|
</tr> |
|
|
|
|
|
</tbody> |
|
|
|
|
|
</table> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
<!-- New line that appears on hover --> |
|
|
<!-- New line that appears on hover --> |
|
|
<div |
|
|
<div |
|
|
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center" |
|
|
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center" |
|
|
@click=" |
|
|
@click=" |
|
|
markStarredProjectChangesAsReadStartingWith( |
|
|
markStarredProjectChangesAsReadStartingWith( |
|
|
projectChange.plan.handleId, |
|
|
projectChange.plan.jwtId!, |
|
|
) |
|
|
) |
|
|
" |
|
|
" |
|
|
> |
|
|
> |
|
|
<span class="inline-block w-8 h-px bg-gray-500 mr-2" /> |
|
|
<span class="inline-block w-8 h-px bg-gray-500 mr-2" /> |
|
|
Click to keep all above as new changes |
|
|
Click to keep all above as unread changes |
|
|
</div> |
|
|
</div> |
|
|
</li> |
|
|
</li> |
|
|
</ul> |
|
|
</ul> |
|
@ -227,6 +286,7 @@ import { |
|
|
OfferSummaryRecord, |
|
|
OfferSummaryRecord, |
|
|
OfferToPlanSummaryRecord, |
|
|
OfferToPlanSummaryRecord, |
|
|
PlanSummaryAndPreviousClaim, |
|
|
PlanSummaryAndPreviousClaim, |
|
|
|
|
|
PlanSummaryRecord, |
|
|
} from "../interfaces/records"; |
|
|
} from "../interfaces/records"; |
|
|
import { |
|
|
import { |
|
|
didInfo, |
|
|
didInfo, |
|
@ -240,6 +300,9 @@ import { logger } from "../utils/logger"; |
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; |
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; |
|
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; |
|
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; |
|
|
import * as databaseUtil from "../db/databaseUtil"; |
|
|
import * as databaseUtil from "../db/databaseUtil"; |
|
|
|
|
|
import * as R from "ramda"; |
|
|
|
|
|
import { PlanActionClaim } from "../interfaces/claims"; |
|
|
|
|
|
import { GenericCredWrapper } from "@/interfaces"; |
|
|
|
|
|
|
|
|
@Component({ |
|
|
@Component({ |
|
|
components: { GiftedDialog, QuickNav, EntityIcon }, |
|
|
components: { GiftedDialog, QuickNav, EntityIcon }, |
|
@ -264,6 +327,10 @@ export default class NewActivityView extends Vue { |
|
|
newStarredProjectChanges: Array<PlanSummaryAndPreviousClaim> = []; |
|
|
newStarredProjectChanges: Array<PlanSummaryAndPreviousClaim> = []; |
|
|
newStarredProjectChangesHitLimit = false; |
|
|
newStarredProjectChangesHitLimit = false; |
|
|
starredProjectIds: Array<string> = []; |
|
|
starredProjectIds: Array<string> = []; |
|
|
|
|
|
planDifferences: Record< |
|
|
|
|
|
string, |
|
|
|
|
|
Record<string, { old: unknown; new: unknown }> |
|
|
|
|
|
> = {}; |
|
|
|
|
|
|
|
|
showOffersDetails = false; |
|
|
showOffersDetails = false; |
|
|
showOffersToUserProjectsDetails = false; |
|
|
showOffersToUserProjectsDetails = false; |
|
@ -323,6 +390,9 @@ export default class NewActivityView extends Vue { |
|
|
this.newStarredProjectChanges = starredProjectChangesData.data; |
|
|
this.newStarredProjectChanges = starredProjectChangesData.data; |
|
|
this.newStarredProjectChangesHitLimit = |
|
|
this.newStarredProjectChangesHitLimit = |
|
|
starredProjectChangesData.hitLimit; |
|
|
starredProjectChangesData.hitLimit; |
|
|
|
|
|
|
|
|
|
|
|
// Analyze differences between current plans and previous claims |
|
|
|
|
|
this.analyzePlanDifferences(this.newStarredProjectChanges); |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
logger.warn("Failed to load starred project changes:", error); |
|
|
logger.warn("Failed to load starred project changes:", error); |
|
|
this.newStarredProjectChanges = []; |
|
|
this.newStarredProjectChanges = []; |
|
@ -349,7 +419,7 @@ export default class NewActivityView extends Vue { |
|
|
// note that we don't update this.lastAckedOfferToUserJwtId in case they |
|
|
// note that we don't update this.lastAckedOfferToUserJwtId in case they |
|
|
// later choose the last one to keep the offers as new |
|
|
// later choose the last one to keep the offers as new |
|
|
this.notify.info( |
|
|
this.notify.info( |
|
|
"The offers are marked as viewed. Click in the list to keep them as new.", |
|
|
"The offers are marked read. Click in the list to keep them unread.", |
|
|
TIMEOUTS.LONG, |
|
|
TIMEOUTS.LONG, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
@ -387,7 +457,7 @@ export default class NewActivityView extends Vue { |
|
|
// note that we don't update this.lastAckedOfferToUserProjectsJwtId in case |
|
|
// note that we don't update this.lastAckedOfferToUserProjectsJwtId in case |
|
|
// they later choose the last one to keep the offers as new |
|
|
// they later choose the last one to keep the offers as new |
|
|
this.notify.info( |
|
|
this.notify.info( |
|
|
"The offers are now marked as viewed. Click in the list to keep them as new.", |
|
|
"The offers are now marked read. Click in the list to keep them unread.", |
|
|
TIMEOUTS.LONG, |
|
|
TIMEOUTS.LONG, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
@ -428,7 +498,7 @@ export default class NewActivityView extends Vue { |
|
|
this.newStarredProjectChanges[0].plan.jwtId, |
|
|
this.newStarredProjectChanges[0].plan.jwtId, |
|
|
}); |
|
|
}); |
|
|
this.notify.info( |
|
|
this.notify.info( |
|
|
"The starred project changes are now marked as viewed. Click in the list to keep them as new.", |
|
|
"The starred project changes are now marked read. Click in the list to keep them unread.", |
|
|
TIMEOUTS.LONG, |
|
|
TIMEOUTS.LONG, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
@ -456,5 +526,310 @@ export default class NewActivityView extends Vue { |
|
|
TIMEOUTS.STANDARD, |
|
|
TIMEOUTS.STANDARD, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Normalizes values for comparison - treats null, undefined, and empty string as equivalent |
|
|
|
|
|
* |
|
|
|
|
|
* @param value The value to normalize |
|
|
|
|
|
* @returns The normalized value (null for null/undefined/empty, otherwise the original value) |
|
|
|
|
|
*/ |
|
|
|
|
|
normalizeValueForComparison(value: unknown): unknown { |
|
|
|
|
|
if (value === null || value === undefined || value === "") { |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
return value; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Analyzes differences between current plans and their previous claims |
|
|
|
|
|
* |
|
|
|
|
|
* Walks through a list of PlanSummaryAndPreviousClaim items and stores the |
|
|
|
|
|
* differences between the previous claim and the current plan. This method |
|
|
|
|
|
* extracts the claim from the wrappedClaimBefore object and compares relevant |
|
|
|
|
|
* fields with the current plan. |
|
|
|
|
|
* |
|
|
|
|
|
* @param planChanges Array of PlanSummaryAndPreviousClaim objects to analyze |
|
|
|
|
|
*/ |
|
|
|
|
|
analyzePlanDifferences(planChanges: Array<PlanSummaryAndPreviousClaim>) { |
|
|
|
|
|
this.planDifferences = {}; |
|
|
|
|
|
|
|
|
|
|
|
for (const planChange of planChanges) { |
|
|
|
|
|
console.log("planChange", planChange); |
|
|
|
|
|
const currentPlan: PlanSummaryRecord = planChange.plan; |
|
|
|
|
|
const wrappedClaim: GenericCredWrapper<PlanActionClaim> = |
|
|
|
|
|
planChange.wrappedClaimBefore; |
|
|
|
|
|
|
|
|
|
|
|
// Extract the actual claim from the wrapped claim |
|
|
|
|
|
let previousClaim: PlanActionClaim; |
|
|
|
|
|
|
|
|
|
|
|
const embeddedClaim: string = wrappedClaim.claim; |
|
|
|
|
|
if ( |
|
|
|
|
|
embeddedClaim && |
|
|
|
|
|
typeof embeddedClaim === "object" && |
|
|
|
|
|
"credentialSubject" in embeddedClaim |
|
|
|
|
|
) { |
|
|
|
|
|
// It's a Verifiable Credential |
|
|
|
|
|
previousClaim = |
|
|
|
|
|
(embeddedClaim.credentialSubject as PlanActionClaim) || embeddedClaim; |
|
|
|
|
|
} else { |
|
|
|
|
|
// It's a direct claim |
|
|
|
|
|
previousClaim = embeddedClaim; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!previousClaim || !currentPlan.handleId) { |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const differences: Record<string, { old: unknown; new: unknown }> = {}; |
|
|
|
|
|
|
|
|
|
|
|
// Compare name |
|
|
|
|
|
const normalizedOldName = this.normalizeValueForComparison( |
|
|
|
|
|
previousClaim.name, |
|
|
|
|
|
); |
|
|
|
|
|
const normalizedNewName = this.normalizeValueForComparison( |
|
|
|
|
|
currentPlan.name, |
|
|
|
|
|
); |
|
|
|
|
|
if (!R.equals(normalizedOldName, normalizedNewName)) { |
|
|
|
|
|
differences.name = { |
|
|
|
|
|
old: previousClaim.name, |
|
|
|
|
|
new: currentPlan.name, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Compare description |
|
|
|
|
|
const normalizedOldDescription = this.normalizeValueForComparison( |
|
|
|
|
|
previousClaim.description, |
|
|
|
|
|
); |
|
|
|
|
|
const normalizedNewDescription = this.normalizeValueForComparison( |
|
|
|
|
|
currentPlan.description, |
|
|
|
|
|
); |
|
|
|
|
|
if (!R.equals(normalizedOldDescription, normalizedNewDescription)) { |
|
|
|
|
|
differences.description = { |
|
|
|
|
|
old: previousClaim.description, |
|
|
|
|
|
new: currentPlan.description, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Compare location (combine latitude and longitude into one row) |
|
|
|
|
|
const oldLat = previousClaim.location?.geo?.latitude; |
|
|
|
|
|
const oldLon = previousClaim.location?.geo?.longitude; |
|
|
|
|
|
const newLat = currentPlan.locLat; |
|
|
|
|
|
const newLon = currentPlan.locLon; |
|
|
|
|
|
|
|
|
|
|
|
if (!R.equals(oldLat, newLat) || !R.equals(oldLon, newLon)) { |
|
|
|
|
|
differences.location = { |
|
|
|
|
|
old: this.formatLocationValue(oldLat, oldLon, true), |
|
|
|
|
|
new: this.formatLocationValue(newLat, newLon, false), |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Compare agent (issuer) |
|
|
|
|
|
const oldAgent = previousClaim.agent?.identifier; |
|
|
|
|
|
const newAgent = currentPlan.agentDid; |
|
|
|
|
|
const normalizedOldAgent = this.normalizeValueForComparison(oldAgent); |
|
|
|
|
|
const normalizedNewAgent = this.normalizeValueForComparison(newAgent); |
|
|
|
|
|
if (!R.equals(normalizedOldAgent, normalizedNewAgent)) { |
|
|
|
|
|
differences.agent = { |
|
|
|
|
|
old: oldAgent, |
|
|
|
|
|
new: newAgent, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Compare start time |
|
|
|
|
|
const oldStartTime = previousClaim.startTime; |
|
|
|
|
|
const newStartTime = currentPlan.startTime; |
|
|
|
|
|
const normalizedOldStartTime = |
|
|
|
|
|
this.normalizeValueForComparison(oldStartTime); |
|
|
|
|
|
const normalizedNewStartTime = |
|
|
|
|
|
this.normalizeValueForComparison(newStartTime); |
|
|
|
|
|
if (!R.equals(normalizedOldStartTime, normalizedNewStartTime)) { |
|
|
|
|
|
differences.startTime = { |
|
|
|
|
|
old: oldStartTime, |
|
|
|
|
|
new: newStartTime, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Compare end time |
|
|
|
|
|
const oldEndTime = previousClaim.endTime; |
|
|
|
|
|
const newEndTime = currentPlan.endTime; |
|
|
|
|
|
const normalizedOldEndTime = this.normalizeValueForComparison(oldEndTime); |
|
|
|
|
|
const normalizedNewEndTime = this.normalizeValueForComparison(newEndTime); |
|
|
|
|
|
if (!R.equals(normalizedOldEndTime, normalizedNewEndTime)) { |
|
|
|
|
|
differences.endTime = { |
|
|
|
|
|
old: oldEndTime, |
|
|
|
|
|
new: newEndTime, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Compare image |
|
|
|
|
|
const oldImage = previousClaim.image; |
|
|
|
|
|
const newImage = currentPlan.image; |
|
|
|
|
|
const normalizedOldImage = this.normalizeValueForComparison(oldImage); |
|
|
|
|
|
const normalizedNewImage = this.normalizeValueForComparison(newImage); |
|
|
|
|
|
if (!R.equals(normalizedOldImage, normalizedNewImage)) { |
|
|
|
|
|
differences.image = { |
|
|
|
|
|
old: oldImage, |
|
|
|
|
|
new: newImage, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Compare url |
|
|
|
|
|
const oldUrl = previousClaim.url; |
|
|
|
|
|
const newUrl = currentPlan.url; |
|
|
|
|
|
const normalizedOldUrl = this.normalizeValueForComparison(oldUrl); |
|
|
|
|
|
const normalizedNewUrl = this.normalizeValueForComparison(newUrl); |
|
|
|
|
|
if (!R.equals(normalizedOldUrl, normalizedNewUrl)) { |
|
|
|
|
|
differences.url = { |
|
|
|
|
|
old: oldUrl, |
|
|
|
|
|
new: newUrl, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Store differences if any were found |
|
|
|
|
|
if (!R.isEmpty(differences)) { |
|
|
|
|
|
this.planDifferences[currentPlan.handleId] = differences; |
|
|
|
|
|
logger.debug( |
|
|
|
|
|
"[NewActivityView] Plan differences found for", |
|
|
|
|
|
currentPlan.handleId, |
|
|
|
|
|
differences, |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
logger.debug( |
|
|
|
|
|
"[NewActivityView] Analyzed", |
|
|
|
|
|
planChanges.length, |
|
|
|
|
|
"plan changes, found differences in", |
|
|
|
|
|
Object.keys(this.planDifferences).length, |
|
|
|
|
|
"plans", |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Gets the differences for a specific plan by handle ID |
|
|
|
|
|
* |
|
|
|
|
|
* @param handleId The handle ID of the plan to get differences for |
|
|
|
|
|
* @returns The differences object or null if no differences found |
|
|
|
|
|
*/ |
|
|
|
|
|
getPlanDifferences( |
|
|
|
|
|
handleId: string, |
|
|
|
|
|
): Record<string, { old: unknown; new: unknown }> | null { |
|
|
|
|
|
return this.planDifferences[handleId] || null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Formats a field value for display in the UI |
|
|
|
|
|
* |
|
|
|
|
|
* @param value The value to format |
|
|
|
|
|
* @returns A human-readable string representation |
|
|
|
|
|
*/ |
|
|
|
|
|
formatFieldValue(value: unknown): string { |
|
|
|
|
|
if (value === null || value === undefined) { |
|
|
|
|
|
return "Not set"; |
|
|
|
|
|
} |
|
|
|
|
|
if (typeof value === "string") { |
|
|
|
|
|
const stringValue = value || "Empty"; |
|
|
|
|
|
|
|
|
|
|
|
// Check if it's a date/time string |
|
|
|
|
|
if (this.isDateTimeString(stringValue)) { |
|
|
|
|
|
return this.formatDateTime(stringValue); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Check if it's a URL |
|
|
|
|
|
if (this.isUrl(stringValue)) { |
|
|
|
|
|
return stringValue; // Keep URLs as-is for now |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return stringValue; |
|
|
|
|
|
} |
|
|
|
|
|
if (typeof value === "number") { |
|
|
|
|
|
return value.toString(); |
|
|
|
|
|
} |
|
|
|
|
|
if (typeof value === "boolean") { |
|
|
|
|
|
return value ? "Yes" : "No"; |
|
|
|
|
|
} |
|
|
|
|
|
// For complex objects, stringify |
|
|
|
|
|
const stringified = JSON.stringify(value); |
|
|
|
|
|
return stringified; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Checks if a string appears to be a date/time string |
|
|
|
|
|
*/ |
|
|
|
|
|
isDateTimeString(value: string): boolean { |
|
|
|
|
|
if (!value) return false; |
|
|
|
|
|
// Check for ISO 8601 format or other common date formats |
|
|
|
|
|
const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?(\.\d{3})?Z?$/; |
|
|
|
|
|
return dateRegex.test(value) || !isNaN(Date.parse(value)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Checks if a string is a URL |
|
|
|
|
|
*/ |
|
|
|
|
|
isUrl(value: string): boolean { |
|
|
|
|
|
if (!value) return false; |
|
|
|
|
|
try { |
|
|
|
|
|
new URL(value); |
|
|
|
|
|
return true; |
|
|
|
|
|
} catch { |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Formats a date/time string for display |
|
|
|
|
|
*/ |
|
|
|
|
|
formatDateTime(value: string): string { |
|
|
|
|
|
try { |
|
|
|
|
|
const date = new Date(value); |
|
|
|
|
|
return date.toLocaleString(); |
|
|
|
|
|
} catch { |
|
|
|
|
|
return value; // Return original if parsing fails |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Gets a human-readable field name for display |
|
|
|
|
|
* |
|
|
|
|
|
* @param fieldName The internal field name |
|
|
|
|
|
* @returns A formatted field name for display |
|
|
|
|
|
*/ |
|
|
|
|
|
getDisplayFieldName(fieldName: string): string { |
|
|
|
|
|
const fieldNameMap: Record<string, string> = { |
|
|
|
|
|
name: "Name", |
|
|
|
|
|
description: "Description", |
|
|
|
|
|
location: "Location", |
|
|
|
|
|
agent: "Agent", |
|
|
|
|
|
startTime: "Start Time", |
|
|
|
|
|
endTime: "End Time", |
|
|
|
|
|
image: "Image", |
|
|
|
|
|
url: "URL", |
|
|
|
|
|
}; |
|
|
|
|
|
return fieldNameMap[fieldName] || fieldName; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Formats location values for display |
|
|
|
|
|
* |
|
|
|
|
|
* @param latitude The latitude value |
|
|
|
|
|
* @param longitude The longitude value |
|
|
|
|
|
* @param isOldValue Whether this is the old value (true) or new value (false) |
|
|
|
|
|
* @returns A formatted location string |
|
|
|
|
|
*/ |
|
|
|
|
|
formatLocationValue( |
|
|
|
|
|
latitude: number | undefined, |
|
|
|
|
|
longitude: number | undefined, |
|
|
|
|
|
isOldValue: boolean = false, |
|
|
|
|
|
): string { |
|
|
|
|
|
if (latitude === undefined && longitude === undefined) { |
|
|
|
|
|
return "Not set"; |
|
|
|
|
|
} |
|
|
|
|
|
// If there's any location data, show generic labels instead of coordinates |
|
|
|
|
|
if (isOldValue) { |
|
|
|
|
|
return "A Location"; |
|
|
|
|
|
} else { |
|
|
|
|
|
return "New Location"; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
</script> |
|
|
</script> |
|
|