# Main design concepts



# Login 

## Socialite for Google

Install the socialite module
````
composer require laravel/socialite
````


## Image folder for custom images

php artisan storage:link 


### Configure the module

Add provider credentials in config/services.php 

````
    'google' => [
        'client_id'     => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect'      => env('GOOGLE_REDIRECT')
    ],    
````

Add the credentials to your .env file 

Add the following redirect in web.php

```
Route::get('/redirect/{provider}', function ($provider) {
....
});
```

Add the following API routes
````
Route::get('/auth/{provider}/redirect', [App\Http\Controllers\SocialAuthController::class, 'redirectToProvider'])->name('api.social.redirect');
Route::get('/auth/{provider}/callback', [App\Http\Controllers\SocialAuthController::class, 'handleProviderCallback'])->name('api.social.callback');
````


## Create project




### Admin Email
Once the project is finalized, an email is sent to the admin user  
The project is finalized by setting the initialised flag to true

```
initialised = true
```

An email is sent when the flag is set to true for the 1st time


## Invitations

### Concept 

The basic flow for a private invitation:   

1) the owner invites a user via email 
```
new invitation record is generated for the user 
```
2) the user clicks on the invitation email 

the invitation record is marked as "visited"
a "new participant" dialog is displayed where the user can enter their name
a new participant with the name and email is created and displayed in the list of participants  




## Language support

### Language sync with WP

The following cookie defines the current selected locale for the WP user
```
wp-wpml_current_language
```
This cookie is used to determine the initial language of the App user 
The App is getting the locale by calling the locale.php service on the public side




# Subscriptions



# New Flow

## cancel response 



# Laravel migrations


Adding new column in existing tables

php artisan make:migration add_deadline_to_polls_table --table=polls



## Form Elements Configuration



custom_fields 
    id
    type
    name
    placeholder
    position
    mandatory

user_fields
    id
    fk custom_fields
    fk users
    fk polls
    value







Questions from Claude


1 How is the public link constructed? The most-used link of all — and unknown 
      let url =
        process.env.VUE_APP_FRONTEND_URI +
        '/pr/' +
        this.poll.id +
        '/' +
        this.publicKey;
        
    publicKey(state) {
      let ownerId = state.poll.user.id;

      let publicKey = ownerId.substring(0, 4) + ownerId;
      console.log('Getters publicKey:', publicKey);
      return publicKey;
    },        


2 In what order does the backend resolve the second segment? A mistake there = privilege escalation   



3 Does the personal link contain participant.id or invitation.id? Blocks the contact-list work   
invitation.id




4 What's stored in the recognition cookie, how long is it valid, per-poll or global? Determines the cross-device solution   
      const curParticipant = {
        poll: this.poll.id,
        participant: participant.id,
        invitation: participant.invitation ? participant.invitation.id : null,
      };
      //const poll = myParticipants.find((item) => item.poll == this.poll.id);

      myParticipants.push(curParticipant);
      localStorage.setItem(
        'kulibri.participants',
        JSON.stringify(myParticipants)
      );
    },


5 ~~Is the adminKey API field the same as the URL segment? In both polls investigated, the legacy value came back even though poll.key was set — per the code, getAdminkey() should apply there~~ Resolved (see MIGRATION-LOG.md, Hop 1): No. The `{key}` URL segment is `getAdminkey()` (hashid-encoded `poll.key`), not the raw `key` column — confirmed empirically via a real poll fetch through the hashids middleware.


6 ~~When is invitation.visited set? Determines whether "hasn't answered yet" is accurate~~ Resolved: `visited` only means "the personal link was opened", not "has answered". Set once by `POST /invitation/visit/{id}`, triggered when the poll page loads via the personal link — before a `Participant` record necessarily exists. No backend code ever reads the field back; it only drives the open/closed envelope icon in the invitation list. Known, unresolved limitation: crawlers/link previews can auto-open the link and produce a false "visited" status (same risk as `participants.temp`, question 9) — deliberately not fixed, see `tests/Feature/InvitationVisitedTest.php`

7 ~~What is participants.temp for?~~ Resolved: an incomplete participant, created when the public link is opened before the name is entered   
9 Are temp rows from people who abandon the flow cleaned up? Do crawlers and link previews create them too? Skews participant counts. Deleted on completion, otherwise unclear   
???????

10 Does completing the poll always create a new participant, or does it merge into an existing one? Determines the behavior when answering twice via the same link   
???????

8 A single-piece link without login or without reference to the poll? Unclear behavior in a security-relevant path   Points 1 through 3 block concrete work and should be clarified first.
?????




Does /pr/{pollId}/{segment} call the same GET /api/poll/{id} as the admin view, or a different endpoint / with a query parameter? This is the more concrete version of question 2 — no longer important only for security reasons, but because Bolt is about to build exactly this call.
the same endpoint



B — How is the temp participant created? Endpoint and payload for creating it, before the first answer is saved.
The participant is created in the backend but not persisted. 

C — How are the individual answers (preferences) stored during the temp phase? The same PUT /api/participant/{id} as for the admin, or a separate path?
The answers are also only kept in the frontend 

D — Which endpoint finalizes it? Entering name/email → the copy-and-delete happens server-side. Payload, and above all: does the response return the new participant.id and invitation.id? Without that, kulibri.participants in localStorage can't be updated correctly — exactly the gap found in question 4.

    async takeOverTempParticipant(participant, curSubmittingParticipant) {
      console.error(
        'takeOverTempParticipant',
        participant,
        curSubmittingParticipant
      );
      // copy the choices to the new participnt
      participant.preference_list = curSubmittingParticipant.preference_list;
      // save it one more time
      console.info('completeSubmitResponse', participant);
      this.completeSubmitResponse(participant, true);

      // and finaly remove the temp participant
      console.info('removeFromLastParticipants', curSubmittingParticipant);
      this.removeFromLastParticipants(curSubmittingParticipant);
      this.$store
        .dispatch('removeParticipant', curSubmittingParticipant)
        .then(() => {
          console.error(
            'takeOverTempParticipant removeFromLastParticipants completed',
            participants
          );

          this.$store.commit('setParticipantBackup', null);

          this.submittingParticipant = null;
          console.error('submittingParticipant = null');
          let participants = this.identifyCurrentUser(this.poll.participants);
          console.error(
            'takeOverTempParticipant identified Current Users:',
            participants
          );

          sortParticipants(participants, this.userId);
          console.error('takeOverTempParticipant sorted partic:', participants);
          this.$store.commit('updateParticipants', participants);
          this.dlgThanksDialog.openModal({ participant });
        });
    },

