@ -280,14 +280,41 @@ import OnboardingDialog from "../components/OnboardingDialog.vue";
import ProjectIcon from "../components/ProjectIcon.vue" ;
import TopMessage from "../components/TopMessage.vue" ;
import UserNameDialog from "../components/UserNameDialog.vue" ;
import * as databaseUtil from "../db/databaseUtil" ;
import { Contact } from "../db/tables/contacts" ;
import { didInfo , getHeaders , getPlanFromCache } from "../libs/endorserServer" ;
import { OfferSummaryRecord , PlanData } from "../interfaces/records" ;
import * as libsUtil from "../libs/util" ;
import { OnboardPage } from "../libs/util" ;
import { logger } from "../utils/logger" ;
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory" ;
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin" ;
import { createNotifyHelpers , TIMEOUTS } from "@/utils/notify" ;
import {
NOTIFY_NO_ACCOUNT_ERROR ,
NOTIFY_PROJECT_LOAD_ERROR ,
NOTIFY_PROJECT_INIT_ERROR ,
NOTIFY_OFFERS_LOAD_ERROR ,
NOTIFY_OFFERS_FETCH_ERROR ,
NOTIFY_CAMERA_SHARE_METHOD ,
} from "@/constants/notifications" ;
/ * *
* Projects View Component
*
* Main dashboard for managing user projects and offers within the TimeSafari platform .
* Provides dual - mode interface for viewing :
* - Personal projects : Ideas and plans created by the user
* - Active offers : Commitments made to help with other projects
*
* Key Features :
* - Infinite scrolling for large datasets
* - Project creation and navigation
* - Offer tracking with confirmation status
* - Onboarding integration for new users
* - Cross - platform compatibility ( web , mobile , desktop )
*
* Security : All API calls are authenticated using user ' s DID
* Privacy : Only user ' s own projects and offers are displayed
* /
@ Component ( {
components : {
EntityIcon ,
@ -298,18 +325,15 @@ import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
TopMessage ,
UserNameDialog ,
} ,
mixins : [ PlatformServiceMixin ] ,
} )
export default class ProjectsView extends Vue {
$notify ! : ( notification : NotificationIface , timeout ? : number ) => void ;
$router ! : Router ;
errNote ( message : string ) {
this . $notify (
{ group : "alert" , type : "danger" , title : "Error" , text : message } ,
5000 ,
) ;
}
notify ! : ReturnType < typeof createNotifyHelpers > ;
/ / U s e r a c c o u n t s t a t e
activeDid = "" ;
allContacts : Array < Contact > = [ ] ;
allMyDids : Array < string > = [ ] ;
@ -317,61 +341,114 @@ export default class ProjectsView extends Vue {
givenName = "" ;
isLoading = false ;
isRegistered = false ;
/ / D a t a c o l l e c t i o n s
offers : OfferSummaryRecord [ ] = [ ] ;
projectNameFromHandleId : Record < string , string > = { } ; / / m a p p i n g f r o m h a n d l e I d t o d e s c r i p t i o n
projects : PlanData [ ] = [ ] ;
/ / U I s t a t e
showOffers = false ;
showProjects = true ;
/ / U t i l i t y i m p o r t s
libsUtil = libsUtil ;
didInfo = didInfo ;
/ * *
* Initializes notification helpers
* /
created ( ) {
this . notify = createNotifyHelpers ( this . $notify ) ;
}
/ * *
* Component initialization
*
* Workflow :
* 1. Load user settings and account information
* 2. Load contacts for displaying offer recipients
* 3. Initialize onboarding dialog if needed
* 4. Load initial project data
*
* Error handling : Shows appropriate user messages for different failure scenarios
* /
async mounted ( ) {
try {
const settings = await databaseUtil . retrieveSettingsForActiveAccount ( ) ;
this . activeDid = settings . activeDid || "" ;
this . apiServer = settings . apiServer || "" ;
this . isRegistered = ! ! settings . isRegistered ;
this . givenName = settings . firstName || "" ;
const platformService = PlatformServiceFactory . getInstance ( ) ;
const queryResult = await platformService . dbQuery (
"SELECT * FROM contacts" ,
) ;
this . allContacts = databaseUtil . mapQueryResultToValues (
queryResult ,
) as unknown as Contact [ ] ;
await this . initializeUserSettings ( ) ;
await this . loadContactsData ( ) ;
await this . initializeUserIdentities ( ) ;
await this . checkOnboardingStatus ( ) ;
await this . loadInitialData ( ) ;
} catch ( err ) {
logger . error ( "Error initializing ProjectsView:" , err ) ;
this . notify . error ( NOTIFY_PROJECT_INIT_ERROR . message , TIMEOUTS . LONG ) ;
}
}
this . allMyDids = await libsUtil . retrieveAccountDids ( ) ;
/ * *
* Loads user settings from active account
* /
private async initializeUserSettings ( ) {
const settings = await this . $accountSettings ( ) ;
this . activeDid = settings . activeDid || "" ;
this . apiServer = settings . apiServer || "" ;
this . isRegistered = ! ! settings . isRegistered ;
this . givenName = settings . firstName || "" ;
}
if ( ! settings . finishedOnboarding ) {
( this . $refs . onboardingDialog as OnboardingDialog ) . open (
OnboardPage . Create ,
) ;
}
/ * *
* Loads contacts data for displaying offer recipients
* /
private async loadContactsData ( ) {
this . allContacts = await this . $getAllContacts ( ) ;
}
if ( this . allMyDids . length === 0 ) {
logger . error ( "No accounts found." ) ;
this . errNote ( "You need an identifier to load your projects." ) ;
} else {
await this . loadProjects ( ) ;
}
} catch ( err ) {
logger . error ( "Error initializing:" , err ) ;
this . errNote ( "Something went wrong loading your projects." ) ;
/ * *
* Initializes user identity information
* /
private async initializeUserIdentities ( ) {
this . allMyDids = await libsUtil . retrieveAccountDids ( ) ;
}
/ * *
* Checks if onboarding dialog should be shown
* /
private async checkOnboardingStatus ( ) {
const settings = await this . $accountSettings ( ) ;
if ( ! settings . finishedOnboarding ) {
( this . $refs . onboardingDialog as OnboardingDialog ) . open (
OnboardPage . Create ,
) ;
}
}
/ * *
* Loads initial project data if user has valid account
* /
private async loadInitialData ( ) {
if ( this . allMyDids . length === 0 ) {
logger . error ( "No accounts found for user" ) ;
this . notify . error ( NOTIFY_NO_ACCOUNT_ERROR . message , TIMEOUTS . LONG ) ;
} else {
await this . loadProjects ( ) ;
}
}
/ * *
* Core project data loader
* @ param url the url used to fetch the data
* @ param token Authorization token
* * /
*
* Fetches project data from the endorser server and populates the projects array .
* Handles authentication , error scenarios , and loading states .
*
* @ param url - The API endpoint URL for fetching project data
* /
async projectDataLoader ( url : string ) {
try {
const headers = await getHeaders ( this . activeDid , this . $notify ) ;
this . isLoading = true ;
const resp = await this . axios . get ( url , { headers } as AxiosRequestConfig ) ;
if ( resp . status === 200 && resp . data . data ) {
const plans : PlanData [ ] = resp . data . data ;
for ( const plan of plans ) {
@ -391,12 +468,11 @@ export default class ProjectsView extends Vue {
resp . status ,
resp . data ,
) ;
this . errNote ( "Failed to get projects from the server." ) ;
this . notify . error ( NOTIFY_PROJECT_LOAD_ERROR . message , TIMEOUTS . LONG ) ;
}
/ / e s l i n t - d i s a b l e - n e x t - l i n e @ t y p e s c r i p t - e s l i n t / n o - e x p l i c i t - a n y
} catch ( error : any ) {
logger . error ( "Got error loading plans:" , error . message || error ) ;
this . errNote ( "Got an error loading projects." ) ;
this . notify . error ( NOTIFY_PROJECT_LOAD_ERROR . message , TIMEOUTS . LONG ) ;
} finally {
this . isLoading = false ;
}
@ -404,8 +480,12 @@ export default class ProjectsView extends Vue {
/ * *
* Data loader used by infinite scroller
* @ param payload is the flag from the InfiniteScroll indicating if it should load
* * /
*
* Implements pagination by loading additional projects when user scrolls to bottom .
* Uses the last project ' s rowId as a cursor for the next batch .
*
* @ param payload - Flag from InfiniteScroll component indicating if more data should be loaded
* /
async loadMoreProjectData ( payload : boolean ) {
if ( this . projects . length > 0 && payload ) {
const latestProject = this . projects [ this . projects . length - 1 ] ;
@ -414,19 +494,24 @@ export default class ProjectsView extends Vue {
}
/ * *
* Load projects initially
* @ param issuerDid of the user
* @ param urlExtra additional url parameters in a string
* * /
* Load projects initially or with pagination
*
* Constructs the API URL for fetching user ' s projects and delegates to projectDataLoader .
*
* @ param urlExtra - Additional URL parameters for pagination ( e . g . , "beforeId=123" )
* /
async loadProjects ( urlExtra : string = "" ) {
const url = ` ${ this . apiServer } /api/v2/report/plansByIssuer? ${ urlExtra } ` ;
await this . projectDataLoader ( url ) ;
}
/ * *
* Handle clicking on a project entry found in the list
* @ param id of the project
* * /
* Handle clicking on a project entry
*
* Navigates to the detailed project view for the selected project .
*
* @ param id - The unique identifier of the project to view
* /
onClickLoadProject ( id : string ) {
const route = {
path : "/project/" + encodeURIComponent ( id ) ,
@ -435,8 +520,10 @@ export default class ProjectsView extends Vue {
}
/ * *
* Handling clicking on the new project button
* * /
* Handle clicking on the new project button
*
* Navigates to the project creation / editing interface .
* /
onClickNewProject ( ) : void {
const route = {
name : "new-edit-project" ,
@ -444,6 +531,13 @@ export default class ProjectsView extends Vue {
this . $router . push ( route ) ;
}
/ * *
* Handle clicking on a claim / offer link
*
* Navigates to the detailed claim view for the selected offer .
*
* @ param jwtId - The JWT identifier of the claim to view
* /
onClickLoadClaim ( jwtId : string ) {
const route = {
path : "/claim/" + encodeURIComponent ( jwtId ) ,
@ -453,17 +547,21 @@ export default class ProjectsView extends Vue {
/ * *
* Core offer data loader
* @ param url the url used to fetch the data
* @ param token Authorization token
* * /
*
* Fetches offer data from the endorser server and populates the offers array .
* Also retrieves associated project names for display purposes .
*
* @ param url - The API endpoint URL for fetching offer data
* /
async offerDataLoader ( url : string ) {
const headers = await getHeaders ( this . activeDid ) ;
try {
this . isLoading = true ;
const resp = await this . axios . get ( url , { headers } as AxiosRequestConfig ) ;
if ( resp . status === 200 && resp . data . data ) {
/ / a d d o n e - b y - o n e a s t h e y r e t r i e v e p r o j e c t n a m e s , p o t e n t i a l l y f r o m t h e s e r v e r
/ / P r o c e s s o f f e r s o n e - b y - o n e t o r e t r i e v e p r o j e c t n a m e s f r o m s e r v e r c a c h e
for ( const offer of resp . data . data ) {
if ( offer . fulfillsPlanHandleId ) {
const project = await getPlanFromCache (
@ -484,37 +582,24 @@ export default class ProjectsView extends Vue {
resp . status ,
resp . data ,
) ;
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error" ,
text : "Failed to get offers from the server." ,
} ,
5000 ,
) ;
this . notify . error ( NOTIFY_OFFERS_LOAD_ERROR . message , TIMEOUTS . LONG ) ;
}
/ / e s l i n t - d i s a b l e - n e x t - l i n e @ t y p e s c r i p t - e s l i n t / n o - e x p l i c i t - a n y
} catch ( error : any ) {
logger . error ( "Got error loading offers:" , error . message || error ) ;
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error" ,
text : "Got an error loading offers." ,
} ,
5000 ,
) ;
this . notify . error ( NOTIFY_OFFERS_FETCH_ERROR . message , TIMEOUTS . LONG ) ;
} finally {
this . isLoading = false ;
}
}
/ * *
* Data loader used by infinite scroller
* @ param payload is the flag from the InfiniteScroll indicating if it should load
* * /
* Data loader used by infinite scroller for offers
*
* Implements pagination by loading additional offers when user scrolls to bottom .
* Uses the last offer ' s jwtId as a cursor for the next batch .
*
* @ param payload - Flag from InfiniteScroll component indicating if more data should be loaded
* /
async loadMoreOfferData ( payload : boolean ) {
if ( this . offers . length > 0 && payload ) {
const latestOffer = this . offers [ this . offers . length - 1 ] ;
@ -523,15 +608,23 @@ export default class ProjectsView extends Vue {
}
/ * *
* Load offers initially
* @ param issuerDid of the user
* @ param urlExtra additional url parameters in a string
* * /
* Load offers initially or with pagination
*
* Constructs the API URL for fetching user ' s offers and delegates to offerDataLoader .
*
* @ param urlExtra - Additional URL parameters for pagination ( e . g . , "&beforeId=123" )
* /
async loadOffers ( urlExtra : string = "" ) {
const url = ` ${ this . apiServer } /api/v2/report/offers?offeredByDid= ${ this . activeDid } ${ urlExtra } ` ;
await this . offerDataLoader ( url ) ;
}
/ * *
* Shows name dialog if needed , then prompts for share method
*
* Ensures user has provided their name before proceeding with contact sharing .
* Uses UserNameDialog component if name is not set .
* /
showNameThenIdDialog ( ) {
if ( ! this . givenName ) {
( this . $refs . userNameDialog as UserNameDialog ) . open ( ( ) => {
@ -542,13 +635,23 @@ export default class ProjectsView extends Vue {
}
}
/ * *
* Prompts user to choose contact sharing method
*
* Presents modal dialog asking if users are nearby with cameras .
* Routes to appropriate sharing method based on user ' s choice :
* - QR code sharing for nearby users with cameras
* - Alternative sharing methods for remote users
*
* Note : Uses raw $notify for complex modal with custom buttons and onNo callback
* /
promptForShareMethod ( ) {
this . $notify (
{
group : "modal" ,
type : "confirm" ,
title : "Are you nearby with cameras?" ,
text : "If so, we'll use those with QR codes to share." ,
title : NOTIFY_CAMERA_SHARE_METHOD . title ,
text : NOTIFY_CAMERA_SHARE_METHOD . text ,
onCancel : async ( ) => { } ,
onNo : async ( ) => {
this . $router . push ( { name : "share-my-contact-info" } ) ;
@ -556,49 +659,68 @@ export default class ProjectsView extends Vue {
onYes : async ( ) => {
this . handleQRCodeClick ( ) ;
} ,
noText : "we will share another way" ,
yesText : "we are nearby with cameras" ,
noText : NOTIFY_CAMERA_SHARE_METHOD . noText ,
yesText : NOTIFY_CAMERA_SHARE_METHOD . yesText ,
} ,
- 1 ,
) ;
}
public computedOfferTabClassNames ( ) {
/ * *
* Computed properties for template logic streamlining
* /
/ * *
* CSS class names for offer tab styling
* @ returns Object with CSS classes based on current tab selection
* /
get offerTabClasses ( ) {
return {
"inline-block" : true ,
"py-3" : true ,
"rounded-t-lg" : true ,
"border-b-2" : true ,
active : this . showOffers ,
"text-black" : this . showOffers ,
"border-black" : this . showOffers ,
"font-semibold" : this . showOffers ,
"text-blue-600" : ! this . showOffers ,
"border-transparent" : ! this . showOffers ,
"hover:border-slate-400" : ! this . showOffers ,
} ;
}
public computedProjectTabClassNames ( ) {
/ * *
* CSS class names for project tab styling
* @ returns Object with CSS classes based on current tab selection
* /
get projectTabClasses ( ) {
return {
"inline-block" : true ,
"py-3" : true ,
"rounded-t-lg" : true ,
"border-b-2" : true ,
active : this . showProjects ,
"text-black" : this . showProjects ,
"border-black" : this . showProjects ,
"font-semibold" : this . showProjects ,
"text-blue-600" : ! this . showProjects ,
"border-transparent" : ! this . showProjects ,
"hover:border-slate-400" : ! this . showProjects ,
} ;
}
/ * *
* Utility methods
* /
/ * *
* Handles QR code sharing functionality with platform detection
*
* Routes to appropriate QR code interface based on current platform :
* - Full QR scanner for native mobile platforms
* - Web - based QR interface for browser environments
* /
private handleQRCodeClick ( ) {
if ( Capacitor . isNativePlatform ( ) ) {
this . $router . push ( { name : "contact-qr-scan-full" } ) ;
@ -606,5 +728,21 @@ export default class ProjectsView extends Vue {
this . $router . push ( { name : "contact-qr" } ) ;
}
}
/ * *
* Legacy method compatibility
* @ deprecated Use computedOfferTabClassNames for backward compatibility
* /
public computedOfferTabClassNames ( ) {
return this . offerTabClasses ;
}
/ * *
* Legacy method compatibility
* @ deprecated Use computedProjectTabClassNames for backward compatibility
* /
public computedProjectTabClassNames ( ) {
return this . projectTabClasses ;
}
}
< / script >