Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/php-sandbox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Build & deploy sandbox

on:
push:
branches: [ "develop" ]
branches: [ "develop", "add-crowd-compatibility" ]
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -69,7 +69,7 @@ jobs:

- name: Tar
run : |
tar czvf artifact.tar.gz migrations public src vendor .env cron.php
tar czv --ignore-failed-read -f artifact.tar.gz migrations public src vendor .env cron.php 2>/dev/null || tar czv -f artifact.tar.gz migrations public src vendor .env cron.php

- name: Upload
run : |
Expand Down
3 changes: 3 additions & 0 deletions migrations/15-add-form-type.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE {prefix}charity_stream ADD COLUMN form_type VARCHAR(50) NOT NULL DEFAULT 'Donation' AFTER form_slug;


6 changes: 6 additions & 0 deletions src/Assets/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ if (cardWidgetForm) {
}
if (goalEl) goalEl.innerHTML = `Objectif : <strong>${goalValue} €</strong>`;
if (pct) pct.textContent = '50%';

const cta = document.getElementById('cardPreviewCta');
if (cta) {
cta.style.color = tagColor;
cta.style.backgroundColor = tagBgColor;
}
}

bindPreviewInputs(
Expand Down
9 changes: 5 additions & 4 deletions src/Controllers/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ public function newStream(Request $request, Response $response): Response
$event = $this->eventRepository->selectByUserAndId($user, $parentEvent);
}

$stream = $this->streamRepository->insert($data['form_slug'], $data['organization_slug'], $data['title'], $event->id ?? null);
$stream = $this->streamRepository->insert($data['form_slug'], $data['organization_slug'], $data['title'], $event->id ?? null, $data['form_type'] ?? 'Donation');
$this->userRepository->insertRight($owner, $stream, null);

if ($event !== null && $parentStyle) {
Expand Down Expand Up @@ -376,7 +376,8 @@ public function editStream(Request $request, Response $response, array $args): R
$availableEvents = $this->eventRepository->selectListByUser($user);
}

$donationUrl = $_SERVER['HA_URL'] . '/associations/' . $charityStream->organization_slug . '/formulaires/' . $charityStream->form_slug;
$formTypeUrlSegment = ($charityStream->form_type === 'CrowdFunding') ? 'collectes' : 'formulaires';
$donationUrl = $_SERVER['HA_URL'] . '/associations/' . $charityStream->organization_slug . '/' . $formTypeUrlSegment . '/' . $charityStream->form_slug;
$routeParser = RouteContext::fromRequest($request)->getRouteParser();

$data = [
Expand Down Expand Up @@ -552,8 +553,8 @@ public function streamAuthCallback(Request $request, Response $response): Respon
// Stocker / mettre à jour les tokens
$this->apiWrapper->storeOrUpdateToken($tokenData);

// Récupérer les formulaires de don
$forms = $this->apiWrapper->getDonationForms($organizationSlug);
// Récupérer les formulaires de don et de crowdfunding
$forms = $this->apiWrapper->getOrganizationForms($organizationSlug, ['Donation', 'CrowdFunding']);
} catch (Exception $e) {
$response->getBody()->write($this->buildCallbackPage(null, [], $e->getMessage()));
return $response;
Expand Down
9 changes: 8 additions & 1 deletion src/Controllers/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ public function new(Request $request, Response $response): Response
$formSlug = $data['form_slug'] ?? null;
$organizationSlug = $data['organization_slug'] ?? null;
$title = $data['title'] ?? null;
$formType = $data['form_type'] ?? 'Donation';

// Valider le form_type
$allowedFormTypes = ['Donation', 'CrowdFunding'];
if (!in_array($formType, $allowedFormTypes)) {
$formType = 'Donation';
}

if (!$ownerEmail || !$formSlug || !$organizationSlug || !$title) {
$response->getBody()->write(json_encode(['error' => 'all fields are mandatory']));
return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
}

$stream = $this->streamRepository->insert($formSlug, $organizationSlug, $title);
$stream = $this->streamRepository->insert($formSlug, $organizationSlug, $title, null, $formType);
$user = $this->userRepository->findOrCreate($ownerEmail);
$this->userRepository->insertRight($user, $stream, null);
$this->userRepository->insertResetToken($user);
Expand Down
327 changes: 183 additions & 144 deletions src/Controllers/WidgetController.php

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/Models/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Stream
public $title;
public $goal;
public $form_slug;
public $form_type = 'Donation';
public $organization_slug;
public $creation_date;
public $last_update;
Expand Down
8 changes: 5 additions & 3 deletions src/Repositories/StreamRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,20 @@ public function selectByUserAndGuid(User $user, string $guid): ?Stream
return $stream ?: null;
}

public function insert(string $form_slug, string $organization_slug, string $title, ?int $parent = null): Stream
public function insert(string $form_slug, string $organization_slug, string $title, ?int $parent = null, string $form_type = 'Donation'): Stream
{
$guid = bin2hex(random_bytes(16));

$this->pdo->beginTransaction();

try {
$query = 'INSERT INTO ' . $this->prefix . 'charity_stream (guid, form_slug, organization_slug, title, charity_event_id)
VALUES (:guid, :form_slug, :organization_slug, :title, :charity_event_id)';
$query = 'INSERT INTO ' . $this->prefix . 'charity_stream (guid, form_slug, form_type, organization_slug, title, charity_event_id)
VALUES (:guid, :form_slug, :form_type, :organization_slug, :title, :charity_event_id)';
$stmt = $this->pdo->prepare($query);
$stmt->execute([
':guid' => $guid,
':form_slug' => $form_slug,
':form_type' => $form_type,
':organization_slug' => $organization_slug,
':title' => $title,
':charity_event_id' => $parent
Expand Down Expand Up @@ -158,6 +159,7 @@ public function insert(string $form_slug, string $organization_slug, string $tit
$stream->id = $id;
$stream->guid = $guid;
$stream->form_slug = $form_slug;
$stream->form_type = $form_type;
$stream->organization_slug = $organization_slug;
$stream->title = $title;
return $stream;
Expand Down
45 changes: 33 additions & 12 deletions src/Services/ApiWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -363,31 +363,49 @@ public function generateAuthorizationUrl(?string $organizationSlug, ?string $red
}

/**
* Récupère la liste des formulaires de don d'une organisation.
* Récupère la liste des formulaires d'une organisation pour les types donnés.
*
* @param string $organizationSlug
* @param array $formTypes Liste des types de formulaires à récupérer (ex: ['Donation', 'CrowdFunding'])
* @return array
*/
public function getDonationForms(string $organizationSlug): array
public function getOrganizationForms(string $organizationSlug, array $formTypes = ['Donation', 'CrowdFunding']): array
{
$tokenData = $this->getOrganizationAccessToken($organizationSlug);

$response = $this->httpRequest('GET', "{$this->apiUrl}/organizations/{$organizationSlug}/forms", [
'query' => [
'formTypes' => 'Donation',
'pageSize' => 50,
],
// Construire la query string manuellement car Guzzle sérialise les arrays
// avec des indices PHP (formTypes[0]=...) que l'API HelloAsso ne supporte pas.
// L'API attend : formTypes=Donation&formTypes=CrowdFunding
$queryParts = [];
foreach ($formTypes as $type) {
$queryParts[] = 'formTypes=' . urlencode($type);
}
$queryParts[] = 'pageSize=50';
$queryString = implode('&', $queryParts);

$response = $this->httpRequest('GET', "{$this->apiUrl}/organizations/{$organizationSlug}/forms?{$queryString}", [
'headers' => [
'Authorization' => 'Bearer ' . $tokenData->access_token,
'accept' => 'application/json',
],
], "la récupération des formulaires de don pour {$organizationSlug}");
], "la récupération des formulaires pour {$organizationSlug}");

$data = $this->decodeJsonResponse($response);

return $data['data'] ?? [];
}

/**
* Récupère la liste des formulaires de don d'une organisation.
*
* @param string $organizationSlug
* @return array
*/
public function getDonationForms(string $organizationSlug): array
{
return $this->getOrganizationForms($organizationSlug, ['Donation']);
}

/**
* Configure le domaine du client API pour une organisation donnée en utilisant un token d'accès valide.
*
Expand Down Expand Up @@ -480,16 +498,18 @@ public function exchangeAuthorizationCode(string $code, string $redirectUri, str
* @param [type] $continuationToken
* @return array
*/
private function getDonationFormOrders(string $organizationSlug, string $donationSlug, string $accessToken, ?string $continuationToken = null): array
private function getDonationFormOrders(string $organizationSlug, string $donationSlug, string $accessToken, ?string $continuationToken = null, string $formType = 'Donation'): array
{
$query = ['withDetails' => 'true', 'sortOrder' => 'asc', 'pageSize' => 100];
if ($continuationToken) {
$query['continuationToken'] = $continuationToken;
}

$formTypePath = $formType ?: 'Donation';

$response = $this->httpRequest(
'GET',
"{$this->apiUrl}/organizations/{$organizationSlug}/forms/donation/{$donationSlug}/orders",
"{$this->apiUrl}/organizations/{$organizationSlug}/forms/{$formTypePath}/{$donationSlug}/orders",
[
'query' => $query,
'headers' => [
Expand All @@ -512,7 +532,7 @@ private function getDonationFormOrders(string $organizationSlug, string $donatio
* @param [type] $continuationToken
* @return array
*/
public function getAllOrders(string $organizationSlug, string $formSlug, int $currentAmount = 0, ?string $continuationToken = null): array
public function getAllOrders(string $organizationSlug, string $formSlug, int $currentAmount = 0, ?string $continuationToken = null, string $formType = 'Donation'): array
{
$previousToken = '';
$donations = [];
Expand All @@ -539,7 +559,8 @@ public function getAllOrders(string $organizationSlug, string $formSlug, int $cu
$organizationSlug,
$formSlug,
$organizationAccessToken->access_token,
$continuationToken
$continuationToken,
$formType
);

if (!isset($formOrdersData['data'])) {
Expand Down
21 changes: 16 additions & 5 deletions src/views/stream/_modal-create-stream.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,24 @@
</div>
<div class="mb-3">
<label for="form_id" class="form-label fw-semibold">
<span class="me-1">📋</span> Slug du formulaire de don
<span class="me-1">📋</span> Slug du formulaire
</label>
<div class="input-group">
<span class="input-group-text text-muted small">…/formulaires/don/</span>
<span class="input-group-text text-muted small" id="manual_form_prefix">…/formulaires/don/</span>
<input type="text" class="form-control" id="form_id" name="form_slug"
value="{{ formSlug }}" placeholder="mon-formulaire-de-don" required>
value="{{ formSlug }}" placeholder="mon-formulaire" required>
</div>
<div class="invalid-feedback">Le slug du formulaire est requis.</div>
</div>
<div class="mb-3">
<label for="form_type" class="form-label fw-semibold">
<span class="me-1">📦</span> Type de formulaire
</label>
<select class="form-select" id="form_type" name="form_type" onchange="updateManualFormPrefix()">
<option value="Donation" selected>Formulaire de don</option>
<option value="CrowdFunding">Crowdfunding</option>
</select>
</div>
<div class="mb-4">
<label for="title" class="form-label fw-semibold">
<span class="me-1">🎯</span> Titre du stream
Expand Down Expand Up @@ -133,7 +142,7 @@
<div id="stepConnect" class="text-center py-2">
<div class="mb-3 p-4 rounded-3 bg-body-tertiary">
<div class="fs-1 mb-2">🔑</div>
<p class="text-muted mb-0">Connectez votre association HelloAsso pour récupérer automatiquement vos formulaires de don.</p>
<p class="text-muted mb-0">Connectez votre association HelloAsso pour récupérer automatiquement vos formulaires de don et de crowdfunding.</p>
</div>
<button type="button" class="btn btn-primary btn-lg px-5 fw-semibold" id="connectAssoBtn" onclick="connectAsso()">
Connecter mon asso
Expand Down Expand Up @@ -191,14 +200,16 @@
{% endif %}

<input type="hidden" id="asso_organization_slug" name="organization_slug">
<input type="hidden" id="asso_form_type" name="form_type" value="Donation">

<div class="mb-3">
<label for="asso_form_slug" class="form-label fw-semibold">
<span class="me-1">📋</span> Formulaire de don
<span class="me-1">📋</span> Formulaire
</label>
<select class="form-select" id="asso_form_slug" name="form_slug" required>
<option value="">— Sélectionner un formulaire —</option>
</select>
<div class="form-text text-muted small" id="asso_form_type_hint"></div>
<div class="invalid-feedback">Veuillez sélectionner un formulaire.</div>
</div>

Expand Down
33 changes: 32 additions & 1 deletion src/views/stream/_scripts-create-stream.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@
if (toastEl) { new bootstrap.Toast(toastEl).show(); }
});

/* ── Mise à jour du préfixe formulaire mode manuel ── */
function updateManualFormPrefix() {
var formType = document.getElementById('form_type').value;
var prefix = document.getElementById('manual_form_prefix');
if (formType === 'CrowdFunding') {
prefix.textContent = '…/collectes/';
} else {
prefix.textContent = '…/formulaires/don/';
}
}

/* ── Sélection du mode (cartes cliquables) ── */
function selectStreamMode(mode) {
var isAsso = (mode === 'asso');
Expand Down Expand Up @@ -114,20 +125,40 @@

var formSelect = document.getElementById('asso_form_slug');
formSelect.innerHTML = '<option value="">— Sélectionner un formulaire —</option>';

var formTypeLabels = {
'Donation': '🎁 Don',
'CrowdFunding': '🚀 Crowdfunding'
};

forms.forEach(function(form) {
var slug = form.slug || form.formSlug || '';
var label = form.privateTitle || form.title || slug;
var formType = form.formType || 'Donation';
var typeLabel = formTypeLabels[formType] || formType;
var opt = document.createElement('option');
opt.value = slug;
opt.textContent = label;
opt.textContent = '[' + typeLabel + '] ' + label;
opt.dataset.title = label;
opt.dataset.formType = formType;
formSelect.appendChild(opt);
});

formSelect.addEventListener('change', function() {
var titleInput = document.getElementById('asso_title');
var formTypeInput = document.getElementById('asso_form_type');
var formTypeHint = document.getElementById('asso_form_type_hint');
var selectedOpt = this.options[this.selectedIndex];
titleInput.value = selectedOpt && selectedOpt.dataset.title ? selectedOpt.dataset.title : '';
var selectedFormType = selectedOpt && selectedOpt.dataset.formType ? selectedOpt.dataset.formType : 'Donation';
formTypeInput.value = selectedFormType;
if (selectedFormType === 'CrowdFunding') {
formTypeHint.textContent = 'Type : Crowdfunding';
} else if (selectedFormType === 'Donation') {
formTypeHint.textContent = 'Type : Formulaire de don';
} else {
formTypeHint.textContent = '';
}
});

setStepActive(2);
Expand Down
11 changes: 10 additions & 1 deletion src/views/stream/edit.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@

<h5 class="d-flex align-items-center gap-2">
Formulaire HelloAsso lié
{% if charityStream.form_type == 'CrowdFunding' %}
<span class="badge bg-info">🚀 Crowdfunding</span>
{% else %}
<span class="badge bg-success">🎁 Don</span>
{% endif %}
<a href="{{ donationUrl }}" target="_blank" class="btn btn-sm btn-outline-primary">🔗 Ouvrir le formulaire</a>
</h5>

Expand Down Expand Up @@ -357,6 +362,10 @@
<span><strong>0</strong> donateurs</span>
</div>
</div>
<a href="{{ donationUrl }}" target="_blank" rel="noopener noreferrer" id="cardPreviewCta"
style="display:inline-flex; align-items:center; gap:6px; font-size:13px; font-weight:600; text-decoration:none; padding:8px 20px; border-radius:999px; width:fit-content; color:{{ cardWidget.tag_color }}; background-color:{{ cardWidget.tag_background_color }};">
❤️ Je participe
</a>
</div>
</div>
</div>
Expand All @@ -373,7 +382,7 @@
<label for="cardIframeCode" class="form-label fw-semibold">Code iframe à intégrer sur votre site :</label>
<div class="input-group">
<input type="text" class="form-control font-monospace" id="cardIframeCode" readonly
value='<iframe src="{{ widgetCardUrl }}" width="720" height="340" frameborder="0" style="border:none;overflow:hidden;" scrolling="no" allowtransparency="true"></iframe>'>
value='<iframe src="{{ widgetCardUrl }}" width="720" height="400" frameborder="0" style="border:none;overflow:hidden;" scrolling="no" allowtransparency="true"></iframe>'>
<button type="button" class="btn btn-outline-secondary" onclick="navigator.clipboard.writeText(document.getElementById('cardIframeCode').value);this.textContent='✅ Copié !';setTimeout(()=>this.textContent='📋 Copier',2000)">📋 Copier</button>
</div>
</div>
Expand Down
Loading
Loading