Compare commits
6 Commits
81dbe4275d
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| ba11ee7120 | |||
| 98893924ac | |||
| 7dff670122 | |||
| 7aef0210fd | |||
| 6d300ac79c | |||
| 87e23ea50f |
@@ -88,7 +88,7 @@ Elle gère les membres, les cotisations, et s'intègre avec des services tiers (
|
||||
| MemberService | Inscription et desactivation de membres |
|
||||
| DolibarrService | Integration ERP via API REST |
|
||||
| ISPConfigMailService | Gestion comptes mail via SOAP |
|
||||
| ISPConfigWebService | Gestion hebergement web via SOAP (avec cache) |
|
||||
| ISPConfigWebService | Gestion hebergement web via SOAP (avec cache). Creation de clients et reassignation de sites |
|
||||
| NextcloudService | Gestion comptes Nextcloud via OCS (avec cache 7 jours) |
|
||||
|
||||
---
|
||||
@@ -101,6 +101,7 @@ Elle gère les membres, les cotisations, et s'intègre avec des services tiers (
|
||||
| `members:cleanup-expired` | Desactive les membres expires (Dolibarr + ISPConfig + Nextcloud). `--dry-run` |
|
||||
| `sync:ispconfig-mail-members` | Lie les membres a leurs comptes mail ISPConfig (@retzien.fr) |
|
||||
| `sync:ispconfig-web-members` | Lie les membres a leurs comptes d'hebergement web |
|
||||
| `ext:create-isp-accounts` | Cree les clients ISPConfig pour les membres et reassigne leurs sites. `--dry-run`, `--force` |
|
||||
| `nextcloud:sync-members` | Lie les membres a leurs comptes Nextcloud |
|
||||
| `memberships:sync-services` | Synchronise les services associes aux membres |
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Roxane
|
||||
|
||||

|
||||
|
||||
Roxane is an open source membership management application designed for associations. It centralizes member management, subscriptions, and integration with self-hosted third-party services (Dolibarr, ISPConfig, Nextcloud, Sympa).
|
||||
|
||||
The project is developed in the context of **Le Retzien Libre**, a non-profit association promoting digital freedom and self-hosting. It is intended to be generic enough to be adapted by other associations with similar needs.
|
||||
|
||||
312
app/Console/Commands/CreateISPWebAccounts.php
Normal file
312
app/Console/Commands/CreateISPWebAccounts.php
Normal file
@@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\IspconfigType;
|
||||
use App\Models\IspconfigMember;
|
||||
use App\Models\Member;
|
||||
use App\Services\ISPConfig\ISPConfigWebService;
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class CreateISPWebAccounts extends Command
|
||||
{
|
||||
protected $signature = 'ext:create-isp-accounts
|
||||
{--dry-run : Simulate without creating accounts}
|
||||
{--force : Force recreation even if client already exists}';
|
||||
|
||||
protected $description = 'Create ISPConfig clients for members and reassign their websites';
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$isDryRun = $this->option('dry-run');
|
||||
$isForce = $this->option('force');
|
||||
|
||||
$this->info('ISPConfig Client Creation & Website Reassignment');
|
||||
|
||||
if ($isDryRun) {
|
||||
$this->warn('DRY RUN MODE - No changes will be made');
|
||||
}
|
||||
|
||||
$ispWeb = new ISPConfigWebService;
|
||||
|
||||
// Récupération de tous les membres ayant un site web
|
||||
$membersQuery = Member::whereNotNull('website_url')
|
||||
->where('website_url', '!=', '');
|
||||
|
||||
$totalMembers = $membersQuery->count();
|
||||
|
||||
if ($totalMembers === 0) {
|
||||
$this->info('No members with websites found');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$totalMembers} members with websites");
|
||||
|
||||
// Récupération de tous les sites web depuis ISPConfig (avec cache)
|
||||
$allWebsites = collect($ispWeb->getAllWebsites());
|
||||
|
||||
// Récupération des clients déjà créés dans Roxane (pour éviter les doublons)
|
||||
$existingClients = IspconfigMember::where('type', IspconfigType::WEB)
|
||||
->whereIn('member_id', $membersQuery->pluck('id'))
|
||||
->get()
|
||||
->keyBy('member_id');
|
||||
|
||||
$progressBar = progress(
|
||||
label: 'Processing members',
|
||||
steps: $totalMembers
|
||||
);
|
||||
|
||||
$progressBar->start();
|
||||
|
||||
// Statistiques de traitement
|
||||
$stats = [
|
||||
'clients_created' => 0,
|
||||
'websites_reassigned' => 0,
|
||||
'errors' => 0,
|
||||
'skipped' => 0,
|
||||
];
|
||||
|
||||
// Résultats détaillés pour le mode dry-run
|
||||
$dryRunResults = [];
|
||||
|
||||
// Traitement par lots de 50 membres
|
||||
$membersQuery->chunk(50, function ($members) use (
|
||||
$ispWeb,
|
||||
$allWebsites,
|
||||
$existingClients,
|
||||
$progressBar,
|
||||
$isDryRun,
|
||||
$isForce,
|
||||
&$stats,
|
||||
&$dryRunResults
|
||||
) {
|
||||
foreach ($members as $member) {
|
||||
try {
|
||||
/** @var IspconfigMember|null $existingClient */
|
||||
$existingClient = $existingClients->get($member->id);
|
||||
|
||||
// Extraction et normalisation des domaines depuis member->website_url
|
||||
$memberDomains = $this->extractDomains($member->website_url);
|
||||
|
||||
if ($memberDomains->isEmpty()) {
|
||||
$this->warn("No valid domains for member: {$member->full_name}");
|
||||
$stats['skipped']++;
|
||||
$progressBar->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recherche des sites web ISPConfig correspondant aux domaines du membre
|
||||
$matchedWebsites = $ispWeb->findWebsitesForDomains($allWebsites, $memberDomains);
|
||||
|
||||
if ($matchedWebsites->isEmpty()) {
|
||||
$this->warn("No ISPConfig websites found for member: {$member->full_name}");
|
||||
$stats['skipped']++;
|
||||
$progressBar->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$email = $member->retzien_email ?? $member->email;
|
||||
$clientInfo = null;
|
||||
|
||||
// Vérification 1 : Le client existe-t-il dans ISPConfig ?
|
||||
$existingIspClient = $ispWeb->findClientByEmail($email);
|
||||
|
||||
if ($existingIspClient && ! $isForce) {
|
||||
// Client trouvé dans ISPConfig par email
|
||||
$clientInfo = $existingIspClient;
|
||||
$this->info("ISPConfig client found for {$member->full_name} with email {$email} (Client ID: {$clientInfo['client_id']})");
|
||||
} elseif ($existingClient && ! $isForce && $existingClient->ispconfig_client_id > 0) {
|
||||
// Client déjà lié dans Roxane (fallback) - uniquement si client_id valide
|
||||
$clientInfo = [
|
||||
'client_id' => (int) $existingClient->ispconfig_client_id,
|
||||
'groupid' => (int) $existingClient->ispconfig_client_id,
|
||||
];
|
||||
$this->info("Member {$member->full_name} already linked to ISPConfig client (ID: {$clientInfo['client_id']})");
|
||||
}
|
||||
|
||||
if (! $isDryRun) {
|
||||
// MODE EXECUTION RÉELLE
|
||||
|
||||
// Création du client ISPConfig si nécessaire
|
||||
if ($clientInfo === null) {
|
||||
$clientInfo = $this->createClient($member, $ispWeb, $email);
|
||||
$stats['clients_created']++;
|
||||
}
|
||||
|
||||
// Réassignation de tous les sites web au client ISPConfig
|
||||
foreach ($matchedWebsites as $website) {
|
||||
$domainId = $website['domain_id'];
|
||||
|
||||
$ispWeb->updateWebsiteClient($domainId, $clientInfo['groupid']);
|
||||
$stats['websites_reassigned']++;
|
||||
|
||||
$this->info("Reassigned {$website['domain']} to client {$clientInfo['client_id']}");
|
||||
}
|
||||
|
||||
// Sauvegarde dans Roxane de la relation membre <-> client ISPConfig
|
||||
$siteData = $matchedWebsites->map(fn ($site) => [
|
||||
'domain_id' => $site['domain_id'],
|
||||
'domain' => $site['domain'],
|
||||
])->toArray();
|
||||
|
||||
IspconfigMember::updateOrCreate(
|
||||
[
|
||||
'member_id' => $member->id,
|
||||
'type' => IspconfigType::WEB,
|
||||
],
|
||||
[
|
||||
'ispconfig_client_id' => $clientInfo['client_id'],
|
||||
'data' => [
|
||||
'sites' => $siteData,
|
||||
],
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// MODE DRY-RUN : Collecte des informations sans modifications
|
||||
|
||||
$willCreateClient = $clientInfo === null;
|
||||
|
||||
if ($willCreateClient) {
|
||||
$stats['clients_created']++;
|
||||
}
|
||||
|
||||
$dryRunResults[] = [
|
||||
'member' => $member->full_name,
|
||||
'email' => $email,
|
||||
'action' => $willCreateClient ? 'CREATE ISP CLIENT' : 'USE EXISTING ISP CLIENT',
|
||||
'client_id' => $clientInfo['client_id'] ?? 'NEW',
|
||||
'websites' => $matchedWebsites->pluck('domain')->implode(', '),
|
||||
'sites_count' => $matchedWebsites->count(),
|
||||
];
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->error("Error processing {$member->full_name}: {$e->getMessage()}");
|
||||
$stats['errors']++;
|
||||
}
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
});
|
||||
|
||||
$progressBar->finish();
|
||||
|
||||
$this->newLine();
|
||||
$this->info('Summary:');
|
||||
$this->table(
|
||||
['Metric', 'Count'],
|
||||
[
|
||||
['Clients created', $stats['clients_created']],
|
||||
['Websites reassigned', $stats['websites_reassigned']],
|
||||
['Skipped', $stats['skipped']],
|
||||
['Errors', $stats['errors']],
|
||||
]
|
||||
);
|
||||
|
||||
if ($isDryRun && ! empty($dryRunResults)) {
|
||||
$this->newLine();
|
||||
$this->info('Dry Run Details:');
|
||||
$this->table(
|
||||
['Member', 'Email', 'Action', 'Client ID', 'Websites', 'Sites Count'],
|
||||
collect($dryRunResults)->map(fn ($r) => [
|
||||
$r['member'],
|
||||
$r['email'],
|
||||
$r['action'],
|
||||
$r['client_id'],
|
||||
$r['websites'],
|
||||
$r['sites_count'],
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return $stats['errors'] > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait les domaines depuis la chaîne website_url (séparés par ;)
|
||||
* et les normalise en minuscules
|
||||
*/
|
||||
private function extractDomains(string $websiteUrl): Collection
|
||||
{
|
||||
return collect(explode(';', $websiteUrl))
|
||||
->map(fn ($url) => $this->normalizeDomain($url))
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise une URL pour extraire uniquement le nom de domaine
|
||||
* Exemple: "https://www.example.com/path" → "www.example.com"
|
||||
*/
|
||||
private function normalizeDomain(string $url): ?string
|
||||
{
|
||||
$url = trim($url);
|
||||
|
||||
if (! str_starts_with($url, 'http')) {
|
||||
$url = 'https://'.$url;
|
||||
}
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
return $host ? strtolower($host) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un nouveau client dans ISPConfig avec toutes les données du membre
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function createClient(Member $member, ISPConfigWebService $ispWeb, string $email): array
|
||||
{
|
||||
$username = $this->generateUsername($member);
|
||||
$password = Str::random(16);
|
||||
|
||||
// Préparation des données client pour ISPConfig
|
||||
$clientData = [
|
||||
'company_name' => $member->company ?? $member->full_name,
|
||||
'contact_name' => $member->full_name,
|
||||
'email' => $email,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'customer_no' => (string) $member->id,
|
||||
'street' => $member->address ?? '',
|
||||
'zip' => $member->zipcode ?? '',
|
||||
'city' => $member->city ?? '',
|
||||
'country' => $member->country ?? 'FR',
|
||||
'telephone' => $member->phone1 ?? '',
|
||||
'mobile' => $member->phone2 ?? '',
|
||||
'internet' => $member->website_url ?? '',
|
||||
];
|
||||
|
||||
// Appel API ISPConfig pour créer le client
|
||||
$clientInfo = $ispWeb->createClient($clientData);
|
||||
|
||||
$this->info("Created ISPConfig client for {$member->full_name} (Client ID: {$clientInfo['client_id']})");
|
||||
|
||||
return $clientInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un nom d'utilisateur pour ISPConfig
|
||||
* Format: prenomnom (en minuscules, sans espaces ni accents)
|
||||
* Exemple: "jeandurand", "johndoe"
|
||||
*/
|
||||
private function generateUsername(Member $member): string
|
||||
{
|
||||
$username = Str::slug($member->firstname.$member->lastname);
|
||||
|
||||
return str_replace('-', '', $username);
|
||||
}
|
||||
}
|
||||
150
app/Console/Commands/MemberRenewalProcessCommand.php
Normal file
150
app/Console/Commands/MemberRenewalProcessCommand.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Services\MemberRenewalService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Console\Command\Command as CommandAlias;
|
||||
|
||||
class MemberRenewalProcessCommand extends Command
|
||||
{
|
||||
protected $signature = 'members:renewal-process
|
||||
{--phase=auto : Phase à exécuter (auto|reminder_1|reminder_2|reminder_3|deactivate)}
|
||||
{--year= : Année de renouvellement (par défaut : année suivante)}
|
||||
{--dry-run : Mode simulation}
|
||||
{email? : Email d\'un adhérent spécifique}';
|
||||
|
||||
protected $description = 'Processus de relances pour renouvellement adhésion';
|
||||
|
||||
public function __construct(
|
||||
protected MemberRenewalService $renewalService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = $this->option('dry-run');
|
||||
$emailFilter = $this->argument('email');
|
||||
$phaseOption = $this->option('phase');
|
||||
$year = $this->option('year') ?? now()->addYear()->year;
|
||||
|
||||
$this->info(
|
||||
$dryRun
|
||||
? 'DRY-RUN activé – aucune modification ne sera effectuée'
|
||||
: 'Mode réel activé'
|
||||
);
|
||||
|
||||
$phase = $phaseOption === 'auto' ? $this->determinePhase() : $phaseOption;
|
||||
|
||||
if ($phase === null) {
|
||||
$this->warn('Mode auto : aucune phase détectée pour la date actuelle');
|
||||
|
||||
return CommandAlias::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Phase détectée : {$phase}");
|
||||
$this->info("Année de renouvellement : {$year}");
|
||||
|
||||
if ($emailFilter) {
|
||||
$this->warn("Mode utilisateur unique : {$emailFilter}");
|
||||
}
|
||||
|
||||
$members = $this->getEligibleMembers($emailFilter);
|
||||
|
||||
if ($members->isEmpty()) {
|
||||
$this->info('Aucun adhérent éligible');
|
||||
|
||||
return CommandAlias::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("{$members->count()} adhérent(s) à traiter");
|
||||
|
||||
foreach ($members as $member) {
|
||||
try {
|
||||
$this->processMember($member, $phase, $year, $dryRun);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Erreur traitement adhérent relance', [
|
||||
'member_id' => $member->id,
|
||||
'phase' => $phase,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->error("Erreur pour {$member->email} : {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info(
|
||||
$dryRun
|
||||
? 'DRY-RUN terminé – aucune action effectuée'
|
||||
: 'Traitement terminé'
|
||||
);
|
||||
|
||||
return CommandAlias::SUCCESS;
|
||||
}
|
||||
|
||||
protected function determinePhase(): ?string
|
||||
{
|
||||
$today = now();
|
||||
|
||||
if ($today->month === 12 && $today->day >= 1 && $today->day <= 10) {
|
||||
return 'reminder_1';
|
||||
}
|
||||
|
||||
if ($today->month === 12 && $today->day >= 15 && $today->day <= 20) {
|
||||
return 'reminder_2';
|
||||
}
|
||||
|
||||
if ($today->month === 1 && $today->day >= 4 && $today->day <= 6) {
|
||||
return 'reminder_3';
|
||||
}
|
||||
|
||||
if ($today->month === 1 && $today->day >= 25 && $today->day <= 31) {
|
||||
return 'deactivate';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getEligibleMembers(?string $emailFilter): \Illuminate\Support\Collection
|
||||
{
|
||||
$query = Member::query()
|
||||
->where('status', 'valid')
|
||||
->whereHas('memberships', function ($q) {
|
||||
$q->where('status', 'active');
|
||||
});
|
||||
|
||||
if ($emailFilter) {
|
||||
$query->where('email', $emailFilter)
|
||||
->orWhere('retzien_email', $emailFilter);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
protected function processMember(Member $member, string $phase, int $year, bool $dryRun): void
|
||||
{
|
||||
$this->line("• {$member->id} - {$member->email}");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info("[DRY-RUN] Phase {$phase}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sent = match ($phase) {
|
||||
'reminder_1' => $this->renewalService->sendReminder1($member, $year),
|
||||
'reminder_2' => $this->renewalService->sendReminder2($member, $year),
|
||||
'reminder_3' => $this->renewalService->sendReminder3($member, $year),
|
||||
'deactivate' => $this->renewalService->deactivateMember($member, $year),
|
||||
default => false,
|
||||
};
|
||||
|
||||
if ($sent) {
|
||||
$this->info(" ✓ Relance envoyée");
|
||||
} else {
|
||||
$this->warn(" → Aucune relance envoyée (Membre non concerné / mail de relance déjà envoyé / adhésion active pour {$year})");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,13 @@ use App\Models\IspconfigMember;
|
||||
use App\Models\Member;
|
||||
use App\Services\ISPConfig\ISPConfigWebService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class SyncISPConfigWebMembers extends Command
|
||||
{
|
||||
protected $signature = 'sync:ispconfig-web-members {--refresh-cache : Vider le cache avant la synchronisation}';
|
||||
|
||||
protected $description = 'Synchronise les services WEB ISPConfig des membres (via member->website_url)';
|
||||
|
||||
/**
|
||||
@@ -19,11 +21,9 @@ class SyncISPConfigWebMembers extends Command
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
//@todo: Retrouver le client_id pour chaque adhérent
|
||||
|
||||
$this->info('Synchronisation ISPConfig WEB (via member->website_url)');
|
||||
|
||||
$ispWeb = new ISPConfigWebService();
|
||||
$ispWeb = new ISPConfigWebService;
|
||||
|
||||
// Vider le cache si demandé
|
||||
if ($this->option('refresh-cache')) {
|
||||
@@ -60,38 +60,23 @@ class SyncISPConfigWebMembers extends Command
|
||||
|
||||
// Extraction des domaines depuis website_url
|
||||
$memberDomains = collect(explode(';', $member->website_url))
|
||||
->map(fn($url) => $this->normalizeDomain($url))
|
||||
->map(fn ($url) => $this->normalizeDomain($url))
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($memberDomains->isEmpty()) {
|
||||
$progressBar->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recherche des sites ISPConfig correspondants
|
||||
$matchedWebsites = $allWebsites->filter(function ($site) use ($memberDomains, $ispWeb) {
|
||||
$siteDomain = strtolower($site['domain']);
|
||||
|
||||
// Vérification du domaine principal
|
||||
if ($memberDomains->contains($siteDomain)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Récupération et vérification des alias (avec cache)
|
||||
$aliases = $ispWeb->getWebsiteAliases($site['domain_id']);
|
||||
foreach ($aliases as $alias) {
|
||||
if ($memberDomains->contains(strtolower($alias))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
$matchedWebsites = $ispWeb->findWebsitesForDomains($allWebsites, $memberDomains);
|
||||
|
||||
if ($matchedWebsites->isEmpty()) {
|
||||
$progressBar->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -104,7 +89,7 @@ class SyncISPConfigWebMembers extends Command
|
||||
$ispWeb
|
||||
) {
|
||||
$domainId = $site['domain_id'];
|
||||
$sysGroupId = $site['sys_groupid'];
|
||||
$sysGroupId = $site['sys_groupid'] ?? null;
|
||||
$domain = $site['domain'];
|
||||
|
||||
// Récupération des alias (avec cache)
|
||||
@@ -112,8 +97,8 @@ class SyncISPConfigWebMembers extends Command
|
||||
|
||||
// Filtrage des bases de données pour ce site
|
||||
$databases = $allDatabases
|
||||
->filter(fn($db) => $db['sys_groupid'] == $sysGroupId)
|
||||
->map(fn($db) => [
|
||||
->filter(fn ($db) => $db['parent_domain_id'] == $domainId)
|
||||
->map(fn ($db) => [
|
||||
'database_id' => $db['database_id'],
|
||||
'database_name' => $db['database_name'],
|
||||
'database_user_id' => $db['database_user_id'],
|
||||
@@ -123,8 +108,8 @@ class SyncISPConfigWebMembers extends Command
|
||||
|
||||
// Filtrage des utilisateurs FTP pour ce site
|
||||
$ftpUsers = $allFtpUsers
|
||||
->filter(fn($ftp) => $ftp['parent_domain_id'] == $domainId)
|
||||
->map(fn($ftp) => [
|
||||
->filter(fn ($ftp) => $ftp['parent_domain_id'] == $domainId)
|
||||
->map(fn ($ftp) => [
|
||||
'ftp_user_id' => $ftp['ftp_user_id'],
|
||||
'username' => $ftp['username'],
|
||||
'dir' => $ftp['dir'],
|
||||
@@ -133,8 +118,8 @@ class SyncISPConfigWebMembers extends Command
|
||||
|
||||
// Filtrage des utilisateurs Shell pour ce site
|
||||
$shellUsers = $allShellUsers
|
||||
->filter(fn($shell) => $shell['parent_domain_id'] == $domainId)
|
||||
->map(fn($shell) => [
|
||||
->filter(fn ($shell) => $shell['parent_domain_id'] == $domainId)
|
||||
->map(fn ($shell) => [
|
||||
'shell_user_id' => $shell['shell_user_id'],
|
||||
'username' => $shell['username'],
|
||||
'shell' => $shell['shell'],
|
||||
@@ -163,7 +148,7 @@ class SyncISPConfigWebMembers extends Command
|
||||
|
||||
return false;
|
||||
})
|
||||
->map(fn($zone) => [
|
||||
->map(fn ($zone) => [
|
||||
'id' => $zone['id'],
|
||||
'origin' => $zone['origin'],
|
||||
'ns' => $zone['ns'],
|
||||
@@ -176,6 +161,8 @@ class SyncISPConfigWebMembers extends Command
|
||||
return [
|
||||
'domain_id' => $domainId,
|
||||
'domain' => $domain,
|
||||
'sys_groupid' => $sysGroupId,
|
||||
'system_group' => $site['system_group'] ?? null,
|
||||
'document_root' => $site['document_root'],
|
||||
'active' => $site['active'],
|
||||
'aliases' => $aliases,
|
||||
@@ -195,6 +182,7 @@ class SyncISPConfigWebMembers extends Command
|
||||
'ispconfig_service_user_id' => $siteData['domain_id'],
|
||||
],
|
||||
[
|
||||
'ispconfig_client_id' => $siteData['sys_groupid'],
|
||||
'data' => $siteData,
|
||||
]
|
||||
);
|
||||
@@ -215,8 +203,8 @@ class SyncISPConfigWebMembers extends Command
|
||||
{
|
||||
$url = trim($url);
|
||||
|
||||
if (!str_starts_with($url, 'http')) {
|
||||
$url = 'https://' . $url;
|
||||
if (! str_starts_with($url, 'http')) {
|
||||
$url = 'https://'.$url;
|
||||
}
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Console\Commands;
|
||||
use App\Models\Member;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class SyncServicesMembers extends Command
|
||||
@@ -31,7 +32,7 @@ class SyncServicesMembers extends Command
|
||||
// Tous les membres ayant une adhésion en court ont les services activés
|
||||
$this->info('Syncing services members...');
|
||||
|
||||
$members = Member::whereIn('status', ['valid', 'pending'] )->get();
|
||||
$members = Member::whereIn('status', ['valid', 'pending'])->get();
|
||||
|
||||
$progressBar = progress(label: 'Syncing services members', steps: $members->count());
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ namespace App\Enums;
|
||||
enum IspconfigType: string
|
||||
{
|
||||
case MAIL = 'mail';
|
||||
case WEB = 'web';
|
||||
case WEB = 'web';
|
||||
case OTHER = 'other';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::MAIL => 'Email',
|
||||
self::WEB => 'Hébergement',
|
||||
self::MAIL => 'Email',
|
||||
self::WEB => 'Hébergement',
|
||||
self::OTHER => 'Autre',
|
||||
};
|
||||
}
|
||||
|
||||
62
app/Filament/Actions/SendRenewalReminderAction.php
Normal file
62
app/Filament/Actions/SendRenewalReminderAction.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Actions;
|
||||
|
||||
use App\Models\Membership;
|
||||
use App\Services\MemberRenewalService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class SendRenewalReminderAction extends Action
|
||||
{
|
||||
public static function make(?string $name = 'send_renewal_reminder'): static
|
||||
{
|
||||
return parent::make($name)
|
||||
->label('Envoyer une relance')
|
||||
->icon('heroicon-o-envelope')
|
||||
->color('primary')
|
||||
->disabled(function () {
|
||||
$today = now();
|
||||
|
||||
return ! ($today->month === 12 || ($today->month === 1 && $today->day <= 31));
|
||||
})
|
||||
->requiresConfirmation()
|
||||
->modalHeading('Envoyer une relance de renouvellement')
|
||||
->modalDescription(fn (Membership $record) => "Envoyer une relance à {$record->member->full_name} ({$record->member->email}) ?")
|
||||
->form([
|
||||
Select::make('reminder_type')
|
||||
->label('Type de relance')
|
||||
->options([
|
||||
'reminder_1' => '1ère relance (début décembre)',
|
||||
'reminder_2' => '2ème relance (mi-décembre)',
|
||||
'reminder_3' => '3ème relance (début janvier)',
|
||||
])
|
||||
->required()
|
||||
->default('reminder_1'),
|
||||
|
||||
TextInput::make('year')
|
||||
->label('Année de renouvellement')
|
||||
->default(now()->addYear()->year)
|
||||
->numeric()
|
||||
->required(),
|
||||
])
|
||||
->action(function (Membership $record, array $data) {
|
||||
$service = app(MemberRenewalService::class);
|
||||
$member = $record->member;
|
||||
$year = (int) $data['year'];
|
||||
|
||||
match ($data['reminder_type']) {
|
||||
'reminder_1' => $service->sendReminder1($member, $year),
|
||||
'reminder_2' => $service->sendReminder2($member, $year),
|
||||
'reminder_3' => $service->sendReminder3($member, $year),
|
||||
};
|
||||
|
||||
Notification::make()
|
||||
->title('Relance envoyée')
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ namespace App\Filament\Resources\MemberGroups;
|
||||
use App\Filament\Resources\MemberGroups\Pages\CreateMemberGroup;
|
||||
use App\Filament\Resources\MemberGroups\Pages\EditMemberGroup;
|
||||
use App\Filament\Resources\MemberGroups\Pages\ListMemberGroups;
|
||||
use App\Filament\Resources\MemberGroups\RelationManagers\MemberRelationManager;
|
||||
use App\Filament\Resources\MemberGroups\RelationManagers\MembersRelationManager;
|
||||
use App\Filament\Resources\MemberGroups\Schemas\MemberGroupForm;
|
||||
use App\Filament\Resources\MemberGroups\Tables\MemberGroupsTable;
|
||||
|
||||
@@ -17,14 +17,13 @@ class MemberGroupForm
|
||||
->components([
|
||||
Section::make(fn (?MemberGroup $record) => $record?->name ?? MemberGroup::getAttributeLabel('name'))
|
||||
->headerActions([
|
||||
Action::make('view-members')
|
||||
Action::make('view-members')
|
||||
->icon('heroicon-o-users')
|
||||
->label(MemberGroup::getAttributeLabel('view_members'))
|
||||
->action(function (MemberGroup $record) {
|
||||
return redirect()->route('filament.admin.resources.members.index', [ 'filters[group][name][values][0]' => $record->id]);
|
||||
return redirect()->route('filament.admin.resources.members.index', ['filters[group][name][values][0]' => $record->id]);
|
||||
})
|
||||
->visible(fn(?MemberGroup $record) => $record !== null)
|
||||
|
||||
->visible(fn (?MemberGroup $record) => $record !== null),
|
||||
|
||||
])
|
||||
->schema([
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\ListmonkMember;
|
||||
use App\Models\Member;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Package;
|
||||
use App\Notifications\MemberRenewalReminderNotification;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
@@ -17,6 +18,7 @@ use Filament\Forms\Components\Toggle;
|
||||
use Filament\Infolists\Components\RepeatableEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Infolists\Components\ViewEntry;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
@@ -83,6 +85,11 @@ class MemberForm
|
||||
->default('physical')
|
||||
->required(),
|
||||
|
||||
Select::make('type_id')
|
||||
->label(Member::getAttributeLabel('type_id'))
|
||||
->relationship('type', 'name')
|
||||
->default(null),
|
||||
|
||||
Select::make('group_id')
|
||||
->label(Member::getAttributeLabel('group_id'))
|
||||
->relationship('group', 'name')
|
||||
@@ -98,6 +105,10 @@ class MemberForm
|
||||
->email()
|
||||
->required(),
|
||||
|
||||
TextInput::make('retzien_email')
|
||||
->label(Member::getAttributeLabel('retzien_email'))
|
||||
->email(),
|
||||
|
||||
TextInput::make('phone1')
|
||||
->label(Member::getAttributeLabel('phone1'))
|
||||
->tel(),
|
||||
@@ -366,8 +377,28 @@ class MemberForm
|
||||
->label(__('members.actions.send_renewal_mail'))
|
||||
->icon('heroicon-o-envelope')
|
||||
->color('primary')
|
||||
->action(function () {
|
||||
// Mail de relance à créer (Job)
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('members.actions.send_renewal_mail'))
|
||||
->modalDescription('Êtes-vous sûr de vouloir envoyer un mail de relance à cet adhérent ?')
|
||||
->action(function (Member $record) {
|
||||
try {
|
||||
$record->notify(new MemberRenewalReminderNotification($record));
|
||||
|
||||
Notification::make()
|
||||
->title('Mail de relance envoyé')
|
||||
->success()
|
||||
->send();
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('Erreur envoi mail de relance', [
|
||||
'member_id' => $record->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
Notification::make()
|
||||
->title('Erreur lors de l\'envoi du mail')
|
||||
->danger()
|
||||
->send();
|
||||
}
|
||||
}),
|
||||
])
|
||||
->extraAttributes(['class' => 'sticky top-4 h-fit']),
|
||||
|
||||
@@ -6,11 +6,9 @@ use App\Models\Member;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\Filter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class MembersTable
|
||||
@@ -83,7 +81,7 @@ class MembersTable
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
])
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class MemberCount extends StatsOverviewWidget
|
||||
->description(Member::getAttributeLabel('widgets.stats.description'))
|
||||
->descriptionIcon('heroicon-o-user-group', IconPosition::Before)
|
||||
->chart([7, 2, 10, 3, 15, 4, 17])
|
||||
->color('primary')
|
||||
->color('primary'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources\Memberships\Schemas;
|
||||
|
||||
use App\Enums\IspconfigType;
|
||||
use App\Filament\Actions\SendRenewalReminderAction;
|
||||
use App\Filament\Actions\ServiceToggleAction;
|
||||
use App\Models\ListmonkMember;
|
||||
use App\Models\Membership;
|
||||
@@ -261,6 +262,12 @@ class MembershipForm
|
||||
->label(Membership::getAttributeLabel('end_date')),
|
||||
])
|
||||
->extraAttributes(['class' => 'sticky top-4 h-fit']),
|
||||
|
||||
Section::make('Relances')
|
||||
->schema([
|
||||
SendRenewalReminderAction::make(),
|
||||
])
|
||||
->extraAttributes(['class' => 'sticky top-4 h-fit']),
|
||||
])
|
||||
->columnSpan(1),
|
||||
])
|
||||
|
||||
@@ -23,9 +23,9 @@ class PackageForm
|
||||
])
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(Package::getAttributeLabel('name'))
|
||||
->required()
|
||||
->default(null),
|
||||
->label(Package::getAttributeLabel('name'))
|
||||
->required()
|
||||
->default(null),
|
||||
TextInput::make('identifier')
|
||||
->label(Package::getAttributeLabel('identifier'))
|
||||
->required()
|
||||
@@ -34,7 +34,7 @@ class PackageForm
|
||||
Textarea::make('description')
|
||||
->label(Package::getAttributeLabel('description'))
|
||||
->default(null),
|
||||
])
|
||||
]),
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class PackagesTable
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
|
||||
@@ -31,7 +31,7 @@ class ServiceForm
|
||||
TextInput::make('icon')
|
||||
->label(Service::getAttributeLabel('icon'))
|
||||
->default(null),
|
||||
])
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class UserResource extends Resource
|
||||
protected static ?string $model = User::class;
|
||||
|
||||
protected static string|null|\UnitEnum $navigationGroup = 'Paramètres';
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
@@ -51,6 +52,7 @@ class UserResource extends Resource
|
||||
{
|
||||
return User::getAttributeLabel('user');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return User::getAttributeLabel('users');
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Forms;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Forms\MembershipRequest;
|
||||
use App\Models\MemberType;
|
||||
use App\Models\Package;
|
||||
use App\Models\Service;
|
||||
use App\Services\MemberService;
|
||||
@@ -18,23 +19,39 @@ class MembershipFormController extends Controller
|
||||
public function create(): Response
|
||||
{
|
||||
$remainingMonths = 13 - now()->month;
|
||||
$currentYear = now()->year;
|
||||
|
||||
$plans = Package::query()
|
||||
->where('is_active', true)
|
||||
->select('id', 'identifier', 'name', 'price', 'description')
|
||||
->get()
|
||||
->map(fn (Package $p) => [
|
||||
'id' => $p->id,
|
||||
'identifier' => $p->identifier,
|
||||
'name' => $p->name,
|
||||
'description' => $p->description,
|
||||
'price' => $p->identifier === 'custom' ? $remainingMonths : (float) $p->price,
|
||||
'months' => $p->identifier === 'custom' ? $remainingMonths : null,
|
||||
]);
|
||||
->map(function (Package $p) use ($remainingMonths) {
|
||||
$months = match ($p->identifier) {
|
||||
'custom' => $remainingMonths,
|
||||
'one-year' => $remainingMonths + 12,
|
||||
'two-years' => $remainingMonths + 24,
|
||||
default => null,
|
||||
};
|
||||
|
||||
return [
|
||||
'id' => $p->id,
|
||||
'identifier' => $p->identifier,
|
||||
'name' => $p->name,
|
||||
'description' => $p->description,
|
||||
'price' => $months ?? (float) $p->price,
|
||||
'months' => $months,
|
||||
];
|
||||
});
|
||||
|
||||
$memberTypes = MemberType::query()
|
||||
->select('id', 'identifier', 'name')
|
||||
->get();
|
||||
|
||||
return Inertia::render('forms/membership', [
|
||||
'plans' => $plans,
|
||||
'services' => Service::query()->select('name', 'description')->get(),
|
||||
'member_types' => $memberTypes,
|
||||
'current_year' => $currentYear,
|
||||
'captcha_question' => $this->generateCaptcha(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -29,10 +29,12 @@ class MembershipRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'member_type' => 'required|string|exists:member_types,identifier',
|
||||
'lastname' => 'required|string|max:255',
|
||||
'firstname' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255',
|
||||
'company' => 'nullable|string|max:255',
|
||||
'desired_retzien_email' => ['nullable', 'string', 'max:64', 'regex:/^[a-z0-9._-]+$/'],
|
||||
'address' => 'required|string|max:255',
|
||||
'zipcode' => ['required', 'digits:5'],
|
||||
'city' => 'required|string|max:255',
|
||||
|
||||
@@ -45,6 +45,7 @@ class Contact extends Model
|
||||
{
|
||||
return __("contacts.fields.$attribute");
|
||||
}
|
||||
|
||||
public function getFullNameAttribute(): string
|
||||
{
|
||||
return "{$this->firstname} {$this->lastname}";
|
||||
|
||||
@@ -13,7 +13,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Member $member
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereData($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereListmonkUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereMemberId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ListmonkMember extends Model
|
||||
|
||||
@@ -19,10 +19,10 @@ use Illuminate\Notifications\Notifiable;
|
||||
* @property string $nature
|
||||
* @property int|null $type_id
|
||||
* @property int|null $group_id
|
||||
* @property string|null $lastname
|
||||
* @property string|null $firstname
|
||||
* @property string|null $lastname
|
||||
* @property string $email
|
||||
* @property string $retzien_email
|
||||
* @property string|null $retzien_email
|
||||
* @property string|null $company
|
||||
* @property string|null $date_of_birth
|
||||
* @property string|null $address
|
||||
@@ -40,17 +40,23 @@ use Illuminate\Notifications\Notifiable;
|
||||
* @property-read \App\Models\MemberGroup|null $group
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\IspconfigMember> $ispconfigs
|
||||
* @property-read int|null $ispconfigs_count
|
||||
* @property-read \App\Models\Membership|null $lastActiveMembership
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ListmonkMember> $listmonkMembers
|
||||
* @property-read int|null $listmonk_members_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Membership> $memberships
|
||||
* @property-read int|null $memberships_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\NextCloudMember> $nextcloudAccounts
|
||||
* @property-read int|null $nextcloud_accounts_count
|
||||
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
|
||||
* @property-read int|null $notifications_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\MemberRenewalReminder> $renewalReminders
|
||||
* @property-read int|null $renewal_reminders_count
|
||||
* @property-read \App\Models\MemberType|null $type
|
||||
* @property-read \App\Models\User|null $user
|
||||
*
|
||||
* @method static \Database\Factories\MemberFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereAddress($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereCity($value)
|
||||
@@ -77,7 +83,8 @@ use Illuminate\Notifications\Notifiable;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereWebsiteUrl($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereZipcode($value)
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member withTrashed(bool $withTrashed = true)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Member extends Model
|
||||
@@ -133,6 +140,11 @@ class Member extends Model
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function type(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MemberType::class);
|
||||
}
|
||||
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MemberGroup::class);
|
||||
@@ -158,6 +170,11 @@ class Member extends Model
|
||||
return $this->hasMany(ListmonkMember::class, 'member_id');
|
||||
}
|
||||
|
||||
public function renewalReminders(): HasMany
|
||||
{
|
||||
return $this->hasMany(MemberRenewalReminder::class);
|
||||
}
|
||||
|
||||
public function lastActiveMembership(): HasOne
|
||||
{
|
||||
return $this->hasOne(Membership::class)->where('status', 'active')->latest();
|
||||
|
||||
@@ -32,12 +32,12 @@ class MemberGroup extends Model
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'identifier'
|
||||
'identifier',
|
||||
];
|
||||
|
||||
public static function getAttributeLabel(string $attribute): string
|
||||
{
|
||||
return __('member_groups.fields.' . $attribute);
|
||||
return __('member_groups.fields.'.$attribute);
|
||||
}
|
||||
|
||||
public function members(): HasMany
|
||||
|
||||
107
app/Models/MemberRenewalReminder.php
Normal file
107
app/Models/MemberRenewalReminder.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $member_id
|
||||
* @property int $renewal_year
|
||||
* @property bool $reminder_1_sent
|
||||
* @property bool $reminder_2_sent
|
||||
* @property bool $reminder_3_sent
|
||||
* @property bool $deactivation_sent
|
||||
* @property \Illuminate\Support\Carbon|null $reminder_1_sent_at
|
||||
* @property \Illuminate\Support\Carbon|null $reminder_2_sent_at
|
||||
* @property \Illuminate\Support\Carbon|null $reminder_3_sent_at
|
||||
* @property \Illuminate\Support\Carbon|null $deactivated_at
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Member $member
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereDeactivatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereDeactivationSent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereMemberId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder1Sent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder1SentAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder2Sent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder2SentAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder3Sent($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder3SentAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereRenewalYear($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class MemberRenewalReminder extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'member_id',
|
||||
'renewal_year',
|
||||
'reminder_1_sent',
|
||||
'reminder_2_sent',
|
||||
'reminder_3_sent',
|
||||
'deactivation_sent',
|
||||
'reminder_1_sent_at',
|
||||
'reminder_2_sent_at',
|
||||
'reminder_3_sent_at',
|
||||
'deactivated_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'reminder_1_sent' => 'boolean',
|
||||
'reminder_2_sent' => 'boolean',
|
||||
'reminder_3_sent' => 'boolean',
|
||||
'deactivation_sent' => 'boolean',
|
||||
'reminder_1_sent_at' => 'datetime',
|
||||
'reminder_2_sent_at' => 'datetime',
|
||||
'reminder_3_sent_at' => 'datetime',
|
||||
'deactivated_at' => 'datetime',
|
||||
'renewal_year' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
public function markAsReminder1Sent(): void
|
||||
{
|
||||
$this->update([
|
||||
'reminder_1_sent' => true,
|
||||
'reminder_1_sent_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function markAsReminder2Sent(): void
|
||||
{
|
||||
$this->update([
|
||||
'reminder_2_sent' => true,
|
||||
'reminder_2_sent_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function markAsReminder3Sent(): void
|
||||
{
|
||||
$this->update([
|
||||
'reminder_3_sent' => true,
|
||||
'reminder_3_sent_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function markAsDeactivated(): void
|
||||
{
|
||||
$this->update([
|
||||
'deactivation_sent' => true,
|
||||
'deactivated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
40
app/Models/MemberType.php
Normal file
40
app/Models/MemberType.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $identifier
|
||||
* @property string $name
|
||||
* @property string|null $description
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereDescription($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereIdentifier($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType withTrashed(bool $withTrashed = true)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class MemberType extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'identifier',
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
}
|
||||
@@ -16,7 +16,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
* @property string $status
|
||||
* @property string|null $validation_date
|
||||
* @property string|null $payment_method
|
||||
* @property string $amount
|
||||
* @property numeric $amount
|
||||
* @property string $payment_status
|
||||
* @property string|null $note_public
|
||||
* @property string|null $note_private
|
||||
@@ -30,7 +30,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
* @property-read \App\Models\Package $package
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Service> $services
|
||||
* @property-read int|null $services_count
|
||||
*
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership query()
|
||||
@@ -52,7 +51,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership whereValidationDate($value)
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Membership extends Model
|
||||
|
||||
@@ -22,7 +22,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NextCloudMember whereMemberId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NextCloudMember whereNextcloudUserId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NextCloudMember whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NextCloudMember whereUsername($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class NextCloudMember extends Model
|
||||
|
||||
@@ -6,6 +6,36 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $identifier
|
||||
* @property string $name
|
||||
* @property string $subject
|
||||
* @property string $body
|
||||
* @property array<array-key, mixed>|null $variables
|
||||
* @property bool $is_active
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @method static \Database\Factories\NotificationTemplateFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereBody($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereIdentifier($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereIsActive($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereSubject($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereVariables($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate withTrashed(bool $withTrashed = true)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class NotificationTemplate extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $identifier
|
||||
* @property string $name
|
||||
* @property string|null $description
|
||||
* @property string $price
|
||||
* @property numeric $price
|
||||
* @property int $is_active
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
@@ -40,7 +39,6 @@ class Package extends Model
|
||||
|
||||
public static function getAttributeLabel(string $attribute): string
|
||||
{
|
||||
return __('packages.fields.' . $attribute);
|
||||
return __('packages.fields.'.$attribute);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ class Service extends Model
|
||||
|
||||
public static function getAttributeLabel(string $attribute): string
|
||||
{
|
||||
return __('services.fields.' . $attribute);
|
||||
return __('services.fields.'.$attribute);
|
||||
}
|
||||
|
||||
public function memberships() : BelongsToMany
|
||||
public function memberships(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Membership::class, 'services_memberships', 'service_id', 'membership_id');
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
* @property-read int|null $permissions_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Spatie\Permission\Models\Role> $roles
|
||||
* @property-read int|null $roles_count
|
||||
*
|
||||
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
|
||||
@@ -47,7 +46,6 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withoutPermission($permissions)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withoutRole($roles, $guard = null)
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class User extends Authenticatable implements FilamentUser
|
||||
|
||||
51
app/Notifications/MemberAccountDeactivatedNotification.php
Normal file
51
app/Notifications/MemberAccountDeactivatedNotification.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\NotificationTemplate;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MemberAccountDeactivatedNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public readonly Member $member,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$template = NotificationTemplate::findByIdentifier('member_account_deactivated');
|
||||
|
||||
$vars = [
|
||||
'member_name' => $this->member->full_name,
|
||||
'app_name' => config('app.front_name'),
|
||||
];
|
||||
|
||||
return (new MailMessage)
|
||||
->subject($template->renderSubject($vars))
|
||||
->view('notifications.mail-template', [
|
||||
'body' => $template->renderBody($vars),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
51
app/Notifications/MemberRenewalReminder2Notification.php
Normal file
51
app/Notifications/MemberRenewalReminder2Notification.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\NotificationTemplate;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MemberRenewalReminder2Notification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public readonly Member $member,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$template = NotificationTemplate::findByIdentifier('member_renewal_reminder_2');
|
||||
|
||||
$vars = [
|
||||
'member_name' => $this->member->full_name,
|
||||
'app_name' => config('app.front_name'),
|
||||
];
|
||||
|
||||
return (new MailMessage)
|
||||
->subject($template->renderSubject($vars))
|
||||
->view('notifications.mail-template', [
|
||||
'body' => $template->renderBody($vars),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
51
app/Notifications/MemberRenewalReminder3Notification.php
Normal file
51
app/Notifications/MemberRenewalReminder3Notification.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\NotificationTemplate;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MemberRenewalReminder3Notification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public readonly Member $member,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$template = NotificationTemplate::findByIdentifier('member_renewal_reminder_3');
|
||||
|
||||
$vars = [
|
||||
'member_name' => $this->member->full_name,
|
||||
'app_name' => config('app.front_name'),
|
||||
];
|
||||
|
||||
return (new MailMessage)
|
||||
->subject($template->renderSubject($vars))
|
||||
->view('notifications.mail-template', [
|
||||
'body' => $template->renderBody($vars),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
51
app/Notifications/MemberRenewalReminderNotification.php
Normal file
51
app/Notifications/MemberRenewalReminderNotification.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\NotificationTemplate;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MemberRenewalReminderNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public readonly Member $member,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$template = NotificationTemplate::findByIdentifier('member_renewal_reminder');
|
||||
|
||||
$vars = [
|
||||
'member_name' => $this->member->full_name,
|
||||
'app_name' => config('app.front_name'),
|
||||
];
|
||||
|
||||
return (new MailMessage)
|
||||
->subject($template->renderSubject($vars))
|
||||
->view('notifications.mail-template', [
|
||||
'body' => $template->renderBody($vars),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use App\Models\MemberGroup;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
|
||||
class MemberGroupPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:MemberGroup');
|
||||
@@ -66,5 +66,4 @@ class MemberGroupPolicy
|
||||
{
|
||||
return $authUser->can('Reorder:MemberGroup');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use App\Models\Member;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
|
||||
class MemberPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:Member');
|
||||
@@ -66,5 +66,4 @@ class MemberPolicy
|
||||
{
|
||||
return $authUser->can('Reorder:Member');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use App\Models\Membership;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
|
||||
class MembershipPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:Membership');
|
||||
@@ -66,5 +66,4 @@ class MembershipPolicy
|
||||
{
|
||||
return $authUser->can('Reorder:Membership');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use App\Models\Package;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
|
||||
class PackagePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:Package');
|
||||
@@ -66,5 +66,4 @@ class PackagePolicy
|
||||
{
|
||||
return $authUser->can('Reorder:Package');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class RolePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:Role');
|
||||
@@ -66,5 +66,4 @@ class RolePolicy
|
||||
{
|
||||
return $authUser->can('Reorder:Role');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
|
||||
class ServicePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:Service');
|
||||
@@ -66,5 +66,4 @@ class ServicePolicy
|
||||
{
|
||||
return $authUser->can('Reorder:Service');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
|
||||
class UserPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:User');
|
||||
@@ -63,5 +63,4 @@ class UserPolicy
|
||||
{
|
||||
return $authUser->can('Reorder:User');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,13 @@ use Illuminate\Support\Facades\Http;
|
||||
class DolibarrService
|
||||
{
|
||||
protected string $baseUrl;
|
||||
|
||||
protected string $htaccessUrl;
|
||||
|
||||
protected string $username;
|
||||
|
||||
protected string $password;
|
||||
|
||||
protected string $apiKey;
|
||||
|
||||
public function __construct()
|
||||
@@ -20,7 +24,7 @@ class DolibarrService
|
||||
$this->htaccessUrl = config('services.dolibarr.htaccess_url');
|
||||
$this->username = config('services.dolibarr.username');
|
||||
$this->password = config('services.dolibarr.password');
|
||||
$this->apiKey = config('services.dolibarr.api_key');
|
||||
$this->apiKey = config('services.dolibarr.api_key');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,21 +34,22 @@ class DolibarrService
|
||||
{
|
||||
return Http::withBasicAuth($this->username, $this->password)
|
||||
->withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
'DOLAPIKEY' => $this->apiKey,
|
||||
'Accept' => 'application/json',
|
||||
'DOLAPIKEY' => $this->apiKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function getAllMembers(int $limit = 400, string $sortField = 't.rowid', string $sortOrder = 'ASC'): array
|
||||
{
|
||||
$response = $this->client()->get($this->baseUrl . '/members', [
|
||||
$response = $this->client()->get($this->baseUrl.'/members', [
|
||||
'sortfield' => $sortField,
|
||||
'sortorder' => $sortOrder,
|
||||
'limit' => $limit,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
|
||||
return $response->json();
|
||||
@@ -52,11 +57,12 @@ class DolibarrService
|
||||
|
||||
/**
|
||||
* Get member subscriptions
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function getMemberSubscriptions(int|string $id): array
|
||||
{
|
||||
$response = $this->client()->get($this->baseUrl . '/members/'. $id . '/subscriptions');
|
||||
$response = $this->client()->get($this->baseUrl.'/members/'.$id.'/subscriptions');
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
@@ -64,19 +70,18 @@ class DolibarrService
|
||||
/**
|
||||
* Update a member with custom data
|
||||
*
|
||||
* @param int|string $id The Dolibarr member ID (rowid)
|
||||
* @param array $data Array of attributes to update (e.g. ['email' => 'new@email.com', 'array_options' => ['options_custom' => 'val']])
|
||||
* @param int|string $id The Dolibarr member ID (rowid)
|
||||
* @param array $data Array of attributes to update (e.g. ['email' => 'new@email.com', 'array_options' => ['options_custom' => 'val']])
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function updateMember(int|string $id, array $data): bool
|
||||
{
|
||||
$response = $this->client()->put(
|
||||
$this->baseUrl . '/members/' . $id,
|
||||
$this->baseUrl.'/members/'.$id,
|
||||
$data
|
||||
);
|
||||
|
||||
return $response->successful();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ class ISPConfigMailService extends ISPConfigService
|
||||
public function getAllMailDomains(): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"ispconfig.mail.domains.all",
|
||||
'ispconfig.mail.domains.all',
|
||||
config('services.ispconfig.cache_ttl'),
|
||||
fn() => $this->call('mail_domain_get', ['primary_id' => -1])
|
||||
fn () => $this->call('mail_domain_get', ['primary_id' => -1])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ class ISPConfigMailService extends ISPConfigService
|
||||
public function getAllMailUsers(): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"ispconfig.mail.users.all",
|
||||
'ispconfig.mail.users.all',
|
||||
config('services.ispconfig.cache_ttl'),
|
||||
fn() => $this->call('mail_user_get', ['primary_id' => -1])
|
||||
fn () => $this->call('mail_user_get', ['primary_id' => -1])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class ISPConfigMailService extends ISPConfigService
|
||||
$allUsers = $this->getAllMailUsers();
|
||||
|
||||
return collect($allUsers)->filter(function ($user) use ($domain) {
|
||||
return str_ends_with($user['email'], '@' . $domain);
|
||||
return str_ends_with($user['email'], '@'.$domain);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class ISPConfigMailService extends ISPConfigService
|
||||
|
||||
$user = collect($allUsers)->firstWhere('email', $email);
|
||||
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class ISPConfigMailService extends ISPConfigService
|
||||
$user = collect($this->getAllMailUsers())
|
||||
->firstWhere('email', $email);
|
||||
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -105,10 +105,10 @@ class ISPConfigMailService extends ISPConfigService
|
||||
|
||||
// On récupère l'enregistrement COMPLET (OBLIGATOIRE)
|
||||
$mailUserRecord = $this->call('mail_user_get', [
|
||||
$mailuserId
|
||||
$mailuserId,
|
||||
]);
|
||||
|
||||
if (!is_array($mailUserRecord)) {
|
||||
if (! is_array($mailUserRecord)) {
|
||||
throw new \RuntimeException('mail_user_get did not return array');
|
||||
}
|
||||
|
||||
@@ -121,11 +121,9 @@ class ISPConfigMailService extends ISPConfigService
|
||||
$result = $this->call('mail_user_update', [
|
||||
0, // client_id (ADMIN)
|
||||
$mailuserId, // primary_id
|
||||
$mailUserRecord // FULL RECORD
|
||||
$mailUserRecord, // FULL RECORD
|
||||
]);
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,15 +4,18 @@ namespace App\Services\ISPConfig;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use SoapClient;
|
||||
use SoapFault;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ISPConfigService
|
||||
{
|
||||
protected ?SoapClient $client = null;
|
||||
|
||||
protected ?string $sessionId = null;
|
||||
|
||||
protected array $config;
|
||||
|
||||
protected string $serverType;
|
||||
|
||||
/**
|
||||
@@ -23,7 +26,7 @@ class ISPConfigService
|
||||
$this->serverType = $serverType;
|
||||
$this->config = config("services.ispconfig.servers.{$serverType}");
|
||||
|
||||
if (!$this->config) {
|
||||
if (! $this->config) {
|
||||
throw new Exception("ISPConfig server configuration not found for: {$serverType}");
|
||||
}
|
||||
}
|
||||
@@ -44,8 +47,8 @@ class ISPConfigService
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
]
|
||||
])
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->sessionId = $this->client->login(
|
||||
@@ -53,17 +56,17 @@ class ISPConfigService
|
||||
$this->config['password']
|
||||
);
|
||||
|
||||
Log::info("ISPConfig connected", [
|
||||
Log::info('ISPConfig connected', [
|
||||
'server' => $this->serverType,
|
||||
'session_id' => $this->sessionId
|
||||
'session_id' => $this->sessionId,
|
||||
]);
|
||||
|
||||
} catch (SoapFault $e) {
|
||||
Log::error("ISPConfig connection failed", [
|
||||
Log::error('ISPConfig connection failed', [
|
||||
'server' => $this->serverType,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
throw new Exception("Failed to connect to ISPConfig: " . $e->getMessage());
|
||||
throw new Exception('Failed to connect to ISPConfig: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +75,11 @@ class ISPConfigService
|
||||
if ($this->client && $this->sessionId) {
|
||||
try {
|
||||
$this->client->logout($this->sessionId);
|
||||
Log::info("ISPConfig disconnected", ['server' => $this->serverType]);
|
||||
Log::info('ISPConfig disconnected', ['server' => $this->serverType]);
|
||||
} catch (SoapFault $e) {
|
||||
Log::warning("ISPConfig logout failed", [
|
||||
Log::warning('ISPConfig logout failed', [
|
||||
'server' => $this->serverType,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -97,21 +100,21 @@ class ISPConfigService
|
||||
|
||||
$result = $this->client->__soapCall($method, $params);
|
||||
|
||||
Log::debug("ISPConfig API call", [
|
||||
Log::debug('ISPConfig API call', [
|
||||
'method' => $method,
|
||||
'server' => $this->serverType,
|
||||
'success' => true
|
||||
'success' => true,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (SoapFault $e) {
|
||||
Log::error("ISPConfig API call failed", [
|
||||
Log::error('ISPConfig API call failed', [
|
||||
'method' => $method,
|
||||
'server' => $this->serverType,
|
||||
'error' => $e->getMessage()
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
throw new Exception("ISPConfig API call failed: " . $e->getMessage());
|
||||
throw new Exception('ISPConfig API call failed: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +129,9 @@ class ISPConfigService
|
||||
public function getAllClients(): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"ispconfig.mail.clients.all",
|
||||
'ispconfig.mail.clients.all',
|
||||
config('services.ispconfig.cache_ttl'),
|
||||
fn() => $this->call('client_get_all')
|
||||
fn () => $this->call('client_get_all')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\ISPConfig;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ISPConfigWebService extends ISPConfigService
|
||||
@@ -12,71 +13,69 @@ class ISPConfigWebService extends ISPConfigService
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getAllWebsites(): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"ispconfig.web.websites.all",
|
||||
'ispconfig.web.websites.all',
|
||||
config('services.ispconfig.cache_ttl'),
|
||||
fn() => $this->call('sites_web_domain_get', ['primary_id' => -1])
|
||||
fn () => $this->call('sites_web_domain_get', ['primary_id' => -1])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getAllDatabases(): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"ispconfig.web.databases.all",
|
||||
'ispconfig.web.databases.all',
|
||||
config('services.ispconfig.cache_ttl'),
|
||||
fn() => $this->call('sites_database_get', ['primary_id' => -1])
|
||||
fn () => $this->call('sites_database_get', ['primary_id' => -1])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getAllFtpUsers(): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"ispconfig.web.ftp.all",
|
||||
'ispconfig.web.ftp.all',
|
||||
config('services.ispconfig.cache_ttl'),
|
||||
fn() => $this->call('sites_ftp_user_get', ['primary_id' => -1])
|
||||
fn () => $this->call('sites_ftp_user_get', ['primary_id' => -1])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getAllShellUsers(): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"ispconfig.web.shell.all",
|
||||
'ispconfig.web.shell.all',
|
||||
config('services.ispconfig.cache_ttl'),
|
||||
fn() => $this->call('sites_shell_user_get', ['primary_id' => -1])
|
||||
fn () => $this->call('sites_shell_user_get', ['primary_id' => -1])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getAllDnsZones(): array
|
||||
{
|
||||
return Cache::remember(
|
||||
"ispconfig.web.dns-zones.all",
|
||||
'ispconfig.web.dns-zones.all',
|
||||
config('services.ispconfig.cache_ttl'),
|
||||
fn() => $this->call('dns_zone_get', ['primary_id' => -1])
|
||||
fn () => $this->call('dns_zone_get', ['primary_id' => -1])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la liste des alias d'un site web
|
||||
*
|
||||
* @param int $domainId
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getWebsiteAliases(int $domainId): array
|
||||
{
|
||||
@@ -98,10 +97,12 @@ class ISPConfigWebService extends ISPConfigService
|
||||
}
|
||||
|
||||
$aliases = array_map('trim', explode(',', $site['alias']));
|
||||
return array_values(array_filter($aliases, fn($alias) => !empty($alias)));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("Erreur lors de la récupération des alias pour le domaine {$domainId}: " . $e->getMessage());
|
||||
return array_values(array_filter($aliases, fn ($alias) => ! empty($alias)));
|
||||
|
||||
} catch (Exception $e) {
|
||||
\Log::error("Erreur lors de la récupération des alias pour le domaine {$domainId}: ".$e->getMessage());
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -111,9 +112,9 @@ class ISPConfigWebService extends ISPConfigService
|
||||
/**
|
||||
* Récupère la liste des bases de données d'un site en filtrant depuis toutes les BDD
|
||||
*
|
||||
* @param int $sysGroupId
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
* @todo : utiliser plutôt domainId => parent_domain_id
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getWebsiteDatabases(int $sysGroupId): array
|
||||
{
|
||||
@@ -122,15 +123,15 @@ class ISPConfigWebService extends ISPConfigService
|
||||
|
||||
// Filtrage par sys_groupid
|
||||
return collect($allDatabases)
|
||||
->filter(fn($db) => $db['sys_groupid'] == $sysGroupId)
|
||||
->map(fn($db) => [
|
||||
->filter(fn ($db) => $db['sys_groupid'] == $sysGroupId)
|
||||
->map(fn ($db) => [
|
||||
'database_id' => $db['database_id'],
|
||||
'database_name' => $db['database_name'],
|
||||
'database_user' => $db['database_user'],
|
||||
'database_type' => $db['type'],
|
||||
'active' => $db['active'],
|
||||
'remote_access' => $db['remote_access'],
|
||||
'remote_ips' => $db['remote_ips'] ?? ''
|
||||
'remote_ips' => $db['remote_ips'] ?? '',
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
@@ -139,9 +140,7 @@ class ISPConfigWebService extends ISPConfigService
|
||||
/**
|
||||
* Récupère la liste des utilisateurs FTP d'un site en filtrant depuis tous les comptes FTP
|
||||
*
|
||||
* @param int $domainId
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getWebsiteFtpUsers(int $domainId): array
|
||||
{
|
||||
@@ -150,15 +149,15 @@ class ISPConfigWebService extends ISPConfigService
|
||||
|
||||
// Filtrage par parent_domain_id
|
||||
return collect($allFtpUsers)
|
||||
->filter(fn($ftp) => $ftp['parent_domain_id'] == $domainId)
|
||||
->map(fn($ftp) => [
|
||||
->filter(fn ($ftp) => $ftp['parent_domain_id'] == $domainId)
|
||||
->map(fn ($ftp) => [
|
||||
'ftp_user_id' => $ftp['ftp_user_id'],
|
||||
'username' => $ftp['username'],
|
||||
'dir' => $ftp['dir'],
|
||||
'quota_size' => $ftp['quota_size'],
|
||||
'active' => $ftp['active'],
|
||||
'uid' => $ftp['uid'],
|
||||
'gid' => $ftp['gid']
|
||||
'gid' => $ftp['gid'],
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
@@ -167,9 +166,7 @@ class ISPConfigWebService extends ISPConfigService
|
||||
/**
|
||||
* Récupère la liste des utilisateurs Shell d'un site en filtrant depuis tous les comptes Shell
|
||||
*
|
||||
* @param int $domainId
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getWebsiteShellUsers(int $domainId): array
|
||||
{
|
||||
@@ -178,8 +175,8 @@ class ISPConfigWebService extends ISPConfigService
|
||||
|
||||
// Filtrage par parent_domain_id
|
||||
return collect($allShellUsers)
|
||||
->filter(fn($shell) => $shell['parent_domain_id'] == $domainId)
|
||||
->map(fn($shell) => [
|
||||
->filter(fn ($shell) => $shell['parent_domain_id'] == $domainId)
|
||||
->map(fn ($shell) => [
|
||||
'shell_user_id' => $shell['shell_user_id'],
|
||||
'username' => $shell['username'],
|
||||
'dir' => $shell['dir'],
|
||||
@@ -189,7 +186,7 @@ class ISPConfigWebService extends ISPConfigService
|
||||
'quota_size' => $shell['quota_size'],
|
||||
'active' => $shell['active'],
|
||||
'chroot' => $shell['chroot'],
|
||||
'ssh_rsa' => !empty($shell['ssh_rsa'])
|
||||
'ssh_rsa' => ! empty($shell['ssh_rsa']),
|
||||
])
|
||||
->values()
|
||||
->toArray();
|
||||
@@ -198,9 +195,7 @@ class ISPConfigWebService extends ISPConfigService
|
||||
/**
|
||||
* Récupère toutes les informations complètes d'un site (alias, BDD, FTP, Shell)
|
||||
*
|
||||
* @param int $domainId
|
||||
* @return array|null
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getWebsiteCompleteInfo(int $domainId): ?array
|
||||
{
|
||||
@@ -218,7 +213,7 @@ class ISPConfigWebService extends ISPConfigService
|
||||
|
||||
// Récupérer les alias
|
||||
$aliases = [];
|
||||
if (!empty($site['alias'])) {
|
||||
if (! empty($site['alias'])) {
|
||||
$aliases = array_values(array_filter(array_map('trim', explode(',', $site['alias']))));
|
||||
}
|
||||
|
||||
@@ -231,7 +226,7 @@ class ISPConfigWebService extends ISPConfigService
|
||||
'aliases' => $aliases,
|
||||
'databases' => $this->getWebsiteDatabases($domainId, $site['sys_groupid']),
|
||||
'ftp_users' => $this->getWebsiteFtpUsers($domainId),
|
||||
'shell_users' => $this->getWebsiteShellUsers($domainId)
|
||||
'shell_users' => $this->getWebsiteShellUsers($domainId),
|
||||
];
|
||||
}
|
||||
);
|
||||
@@ -239,9 +234,6 @@ class ISPConfigWebService extends ISPConfigService
|
||||
|
||||
/**
|
||||
* Vide le cache pour un domaine spécifique
|
||||
*
|
||||
* @param int $domainId
|
||||
* @return void
|
||||
*/
|
||||
public function clearDomainCache(int $domainId): void
|
||||
{
|
||||
@@ -251,16 +243,177 @@ class ISPConfigWebService extends ISPConfigService
|
||||
|
||||
/**
|
||||
* Vide tout le cache ISPConfig Web
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearAllCache(): void
|
||||
{
|
||||
Cache::forget("ispconfig.web.websites.all");
|
||||
Cache::forget("ispconfig.web.databases.all");
|
||||
Cache::forget("ispconfig.web.ftp.all");
|
||||
Cache::forget("ispconfig.web.shell.all");
|
||||
Cache::forget("ispconfig.web.dns-zones.all");
|
||||
Cache::forget("ispconfig.web.domain-alias.all");
|
||||
Cache::forget('ispconfig.web.websites.all');
|
||||
Cache::forget('ispconfig.web.databases.all');
|
||||
Cache::forget('ispconfig.web.ftp.all');
|
||||
Cache::forget('ispconfig.web.shell.all');
|
||||
Cache::forget('ispconfig.web.dns-zones.all');
|
||||
Cache::forget('ispconfig.web.domain-alias.all');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find websites matching given domains (including aliases)
|
||||
*/
|
||||
public function findWebsitesForDomains(\Illuminate\Support\Collection $allWebsites, \Illuminate\Support\Collection $domains): \Illuminate\Support\Collection
|
||||
{
|
||||
return $allWebsites->filter(function ($site) use ($domains) {
|
||||
$siteDomain = strtolower($site['domain']);
|
||||
|
||||
if ($domains->contains($siteDomain)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$aliases = $this->getWebsiteAliases($site['domain_id']);
|
||||
foreach ($aliases as $alias) {
|
||||
if ($domains->contains(strtolower($alias))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a client by email using sys_groupid from existing websites
|
||||
* Since ISPConfig API doesn't provide a direct way to search clients by email,
|
||||
* we search through websites to find the sys_groupid (client group)
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function findClientByEmail(string $email): ?array
|
||||
{
|
||||
$allWebsites = $this->getAllWebsites();
|
||||
$normalizedEmail = strtolower(trim($email));
|
||||
|
||||
foreach ($allWebsites as $website) {
|
||||
$sysGroupId = $website['sys_groupid'] ?? null;
|
||||
$systemGroup = $website['system_group'] ?? null;
|
||||
|
||||
if (! $sysGroupId || ! $systemGroup) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$clientData = $this->call('client_get_by_groupid', [$sysGroupId]);
|
||||
|
||||
if (! empty($clientData) && isset($clientData['email'])) {
|
||||
if (strtolower(trim($clientData['email'])) === $normalizedEmail) {
|
||||
return [
|
||||
'client_id' => $clientData['client_id'] ?? $sysGroupId,
|
||||
'groupid' => $sysGroupId,
|
||||
'system_group' => $systemGroup,
|
||||
'email' => $clientData['email'],
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new ISPConfig client
|
||||
* Returns an array with client_id and groupid
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function createClient(array $clientData): array
|
||||
{
|
||||
$resellerId = 1;
|
||||
|
||||
$defaultParams = [
|
||||
'limit_maildomain' => -1,
|
||||
'limit_mailbox' => -1,
|
||||
'limit_mailalias' => -1,
|
||||
'limit_mailaliasdomain' => -1,
|
||||
'limit_mailforward' => -1,
|
||||
'limit_mailcatchall' => -1,
|
||||
'limit_mailrouting' => 0,
|
||||
'limit_mail_wblist' => 0,
|
||||
'limit_mailfilter' => -1,
|
||||
'limit_fetchmail' => -1,
|
||||
'limit_mailquota' => -1,
|
||||
'limit_spamfilter_wblist' => 0,
|
||||
'limit_spamfilter_user' => 0,
|
||||
'limit_spamfilter_policy' => 1,
|
||||
'default_webserver' => 1,
|
||||
'limit_web_ip' => '',
|
||||
'limit_web_domain' => -1,
|
||||
'limit_web_quota' => -1,
|
||||
'web_php_options' => 'no,fast-cgi,cgi,mod,suphp',
|
||||
'limit_web_subdomain' => -1,
|
||||
'limit_web_aliasdomain' => -1,
|
||||
'limit_ftp_user' => -1,
|
||||
'limit_shell_user' => 0,
|
||||
'ssh_chroot' => 'no,jailkit,ssh-chroot',
|
||||
'limit_webdav_user' => 0,
|
||||
'default_dnsserver' => 1,
|
||||
'limit_dns_zone' => -1,
|
||||
'limit_dns_slave_zone' => -1,
|
||||
'limit_dns_record' => -1,
|
||||
'default_dbserver' => 1,
|
||||
'limit_database' => -1,
|
||||
'limit_cron' => 0,
|
||||
'limit_cron_type' => 'url',
|
||||
'limit_cron_frequency' => 5,
|
||||
'limit_traffic_quota' => -1,
|
||||
'limit_client' => 0,
|
||||
'parent_client_id' => 0,
|
||||
'language' => 'fr',
|
||||
'usertheme' => 'default',
|
||||
'template_master' => 0,
|
||||
'template_additional' => '',
|
||||
'created_at' => 0,
|
||||
'default_mailserver' => 1,
|
||||
];
|
||||
|
||||
$params = array_merge($defaultParams, $clientData);
|
||||
|
||||
$clientId = $this->call('client_add', [$resellerId, $params]);
|
||||
|
||||
$client = $this->call('client_get', [(int) $clientId]);
|
||||
|
||||
$this->clearAllCache();
|
||||
|
||||
return [
|
||||
'client_id' => (int) $clientId,
|
||||
'groupid' => (int) ($client['groupid'] ?? $clientId),
|
||||
'system_group' => 'client'.$clientId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update website to assign it to a different client
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function updateWebsiteClient(int $domainId, int $newClientId): bool
|
||||
{
|
||||
$websiteRecord = $this->call('sites_web_domain_get', ['domain_id' => $domainId]);
|
||||
|
||||
if (! is_array($websiteRecord) || empty($websiteRecord)) {
|
||||
throw new \RuntimeException("Website with domain_id {$domainId} not found");
|
||||
}
|
||||
|
||||
// sys_groupid n'est pas trouvable dans les données client
|
||||
// $websiteRecord['sys_groupid'] = $newClientId;
|
||||
$websiteRecord['system_group'] = 'client'.$newClientId;
|
||||
|
||||
$result = $this->call('sites_web_domain_update', [
|
||||
0,
|
||||
$domainId,
|
||||
$websiteRecord,
|
||||
]);
|
||||
|
||||
$this->clearAllCache();
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
}
|
||||
|
||||
152
app/Services/MemberRenewalService.php
Normal file
152
app/Services/MemberRenewalService.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\MemberRenewalReminder;
|
||||
use App\Notifications\MemberAccountDeactivatedNotification;
|
||||
use App\Notifications\MemberRenewalReminder2Notification;
|
||||
use App\Notifications\MemberRenewalReminder3Notification;
|
||||
use App\Notifications\MemberRenewalReminderNotification;
|
||||
use App\Services\Dolibarr\DolibarrService;
|
||||
|
||||
class MemberRenewalService
|
||||
{
|
||||
public function __construct(
|
||||
protected DolibarrService $dolibarr,
|
||||
protected MemberService $memberService
|
||||
) {}
|
||||
|
||||
public function sendReminder1(Member $member, int $year): bool
|
||||
{
|
||||
$reminder = MemberRenewalReminder::firstOrCreate(
|
||||
['member_id' => $member->id, 'renewal_year' => $year],
|
||||
);
|
||||
|
||||
if ($reminder->reminder_1_sent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hasActiveMembershipForYear($member, $year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->shouldSendRenewalForYear($member, $year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$member->notify(new MemberRenewalReminderNotification($member));
|
||||
|
||||
$reminder->markAsReminder1Sent();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sendReminder2(Member $member, int $year): bool
|
||||
{
|
||||
$reminder = MemberRenewalReminder::firstOrCreate(
|
||||
['member_id' => $member->id, 'renewal_year' => $year],
|
||||
);
|
||||
|
||||
if ($reminder->reminder_2_sent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hasActiveMembershipForYear($member, $year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->shouldSendRenewalForYear($member, $year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$member->notify(new MemberRenewalReminder2Notification($member));
|
||||
|
||||
$reminder->markAsReminder2Sent();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sendReminder3(Member $member, int $year): bool
|
||||
{
|
||||
$reminder = MemberRenewalReminder::firstOrCreate(
|
||||
['member_id' => $member->id, 'renewal_year' => $year],
|
||||
);
|
||||
|
||||
if ($reminder->reminder_3_sent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hasActiveMembershipForYear($member, $year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->shouldSendRenewalForYear($member, $year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$member->notify(new MemberRenewalReminder3Notification($member));
|
||||
|
||||
$reminder->markAsReminder3Sent();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deactivateMember(Member $member, int $year): bool
|
||||
{
|
||||
$reminder = MemberRenewalReminder::firstOrCreate(
|
||||
['member_id' => $member->id, 'renewal_year' => $year],
|
||||
);
|
||||
|
||||
if ($reminder->deactivation_sent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->hasActiveMembershipForYear($member, $year)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->memberService->deactivateMember($member);
|
||||
|
||||
$member->notify(new MemberAccountDeactivatedNotification($member));
|
||||
|
||||
$reminder->markAsDeactivated();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function hasActiveMembershipForYear(Member $member, int $year): bool
|
||||
{
|
||||
$yearStart = "{$year}-01-01";
|
||||
$yearEnd = "{$year}-12-31";
|
||||
|
||||
return $member->memberships()
|
||||
->where('status', 'active')
|
||||
->where(function ($query) use ($yearStart, $yearEnd) {
|
||||
$query->where(function ($q) use ($yearStart, $yearEnd) {
|
||||
$q->where('start_date', '<=', $yearEnd)
|
||||
->where(function ($q2) use ($yearStart) {
|
||||
$q2->whereNull('end_date')
|
||||
->orWhere('end_date', '>=', $yearStart);
|
||||
});
|
||||
});
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
protected function shouldSendRenewalForYear(Member $member, int $year): bool
|
||||
{
|
||||
$currentMembership = $member->memberships()
|
||||
->where('status', 'active')
|
||||
->orderBy('end_date', 'desc')
|
||||
->first();
|
||||
|
||||
if (! $currentMembership || ! $currentMembership->end_date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$expirationYear = (int) date('Y', strtotime($currentMembership->end_date));
|
||||
|
||||
return $expirationYear === ($year - 1);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Services;
|
||||
use App\Events\MemberRegistered;
|
||||
use App\Models\Member;
|
||||
use App\Models\MemberGroup;
|
||||
use App\Models\MemberType;
|
||||
use App\Models\Package;
|
||||
use App\Notifications\MemberDeactivatedAdminNotification;
|
||||
use App\Notifications\MemberDeactivatedMemberNotification;
|
||||
@@ -22,11 +23,14 @@ class MemberService
|
||||
// Check if the member already exists
|
||||
$member = Member::where('email', $data['email'])->first();
|
||||
|
||||
$memberType = MemberType::where('identifier', $data['member_type'])->first();
|
||||
|
||||
if (! $member) {
|
||||
// Create a new member
|
||||
$member = new Member;
|
||||
$member->status = 'pending';
|
||||
$member->nature = 'physical';
|
||||
$member->type_id = $memberType?->id;
|
||||
$member->group_id = MemberGroup::where('identifier', 'website')->first()->id ?? null;
|
||||
$member->lastname = $data['lastname'];
|
||||
$member->firstname = $data['firstname'];
|
||||
@@ -37,6 +41,11 @@ class MemberService
|
||||
$member->city = $data['city'];
|
||||
$member->country = 'FR';
|
||||
$member->phone1 = $data['phone1'];
|
||||
|
||||
if (! empty($data['desired_retzien_email'])) {
|
||||
$member->retzien_email = $data['desired_retzien_email'].'@retzien.fr';
|
||||
}
|
||||
|
||||
$member->save();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,31 +22,34 @@ class NextcloudService
|
||||
'OCS-APIRequest' => 'true',
|
||||
'Accept' => 'application/json',
|
||||
])
|
||||
->baseUrl(config('services.nextcloud.url') . '/ocs/v1.php');
|
||||
->baseUrl(config('services.nextcloud.url').'/ocs/v1.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable user by email
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function disableUserByEmail(string $email): void
|
||||
{
|
||||
$userId = $this->findUserIdByEmail($email);
|
||||
|
||||
if (!$userId) {
|
||||
Log::warning("Utilisateur Nextcloud introuvable", ['email' => $email]);
|
||||
if (! $userId) {
|
||||
Log::warning('Utilisateur Nextcloud introuvable', ['email' => $email]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$response = $this->http->put("/cloud/users/{$userId}/disable");
|
||||
|
||||
if (!$response->successful()) {
|
||||
if (! $response->successful()) {
|
||||
throw new \RuntimeException("Erreur désactivation Nextcloud {$userId}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Desable user by id
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function disableUserById(string $userId): void
|
||||
@@ -60,7 +63,7 @@ class NextcloudService
|
||||
protected function findUserIdByEmail(string $email): ?string
|
||||
{
|
||||
return Cache::remember(
|
||||
'nextcloud.user_id.' . md5($email),
|
||||
'nextcloud.user_id.'.md5($email),
|
||||
now()->addDays(7),
|
||||
function () use ($email) {
|
||||
return $this->resolveUserIdByEmail($email);
|
||||
@@ -110,6 +113,4 @@ class NextcloudService
|
||||
->get("/cloud/users/{$userId}")
|
||||
->json('ocs.data') ?? [];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
BIN
assets/LRL-roxane.png
Normal file
BIN
assets/LRL-roxane.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -29,7 +29,7 @@ return new class extends Migration
|
||||
'valid',
|
||||
'pending',
|
||||
'cancelled',
|
||||
'excluded'
|
||||
'excluded',
|
||||
])->default('draft');
|
||||
|
||||
// Nature
|
||||
|
||||
@@ -15,7 +15,7 @@ return new class extends Migration
|
||||
$table->id();
|
||||
$table->foreignId('service_id')->constrained('services')->onDelete('cascade');
|
||||
$table->foreignId('membership_id')->constrained('memberships')->onDelete('cascade');
|
||||
//$table->json('data')->nullable();
|
||||
// $table->json('data')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('member_renewal_reminders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
|
||||
$table->year('renewal_year');
|
||||
|
||||
$table->boolean('reminder_1_sent')->default(false);
|
||||
$table->boolean('reminder_2_sent')->default(false);
|
||||
$table->boolean('reminder_3_sent')->default(false);
|
||||
$table->boolean('deactivation_sent')->default(false);
|
||||
|
||||
$table->timestamp('reminder_1_sent_at')->nullable();
|
||||
$table->timestamp('reminder_2_sent_at')->nullable();
|
||||
$table->timestamp('reminder_3_sent_at')->nullable();
|
||||
$table->timestamp('deactivated_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['member_id', 'renewal_year']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('member_renewal_reminders');
|
||||
}
|
||||
};
|
||||
@@ -10,6 +10,7 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
$this->call([
|
||||
MemberGroupSeeder::class,
|
||||
MemberTypeSeeder::class,
|
||||
PackageSeeder::class,
|
||||
ServiceSeeder::class,
|
||||
UserSeeder::class,
|
||||
|
||||
40
database/seeders/MemberTypeSeeder.php
Normal file
40
database/seeders/MemberTypeSeeder.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\MemberType;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class MemberTypeSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$types = [
|
||||
[
|
||||
'identifier' => 'individual',
|
||||
'name' => 'Particulier',
|
||||
'description' => 'Personne physique',
|
||||
],
|
||||
[
|
||||
'identifier' => 'company',
|
||||
'name' => 'Entreprise',
|
||||
'description' => 'Personne morale (société, auto-entrepreneur, etc.)',
|
||||
],
|
||||
[
|
||||
'identifier' => 'association',
|
||||
'name' => 'Association',
|
||||
'description' => 'Association loi 1901',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($types as $type) {
|
||||
MemberType::updateOrCreate(
|
||||
['identifier' => $type['identifier']],
|
||||
[
|
||||
'name' => $type['name'],
|
||||
'description' => $type['description'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,79 @@ class NotificationTemplateSeeder extends Seeder
|
||||
]
|
||||
);
|
||||
|
||||
NotificationTemplate::updateOrCreate(
|
||||
['identifier' => 'member_renewal_reminder'],
|
||||
[
|
||||
'name' => 'Relance renouvellement — 1ère relance (début décembre)',
|
||||
'subject' => 'Renouvellement de votre adhésion pour l\'année à venir — {app_name}',
|
||||
'body' => '<p>Bonjour {member_name},</p>'
|
||||
.'<p>L\'année touche à sa fin et nous vous invitons à renouveler votre adhésion pour l\'année prochaine.</p>'
|
||||
.'<p>Cela vous permettra de continuer à bénéficier de l\'ensemble de nos services sans interruption.</p>'
|
||||
.'<p>Nous restons à votre disposition pour toute question.</p>'
|
||||
.'<p>Cordialement,<br>L\'équipe {app_name}</p>',
|
||||
'variables' => [
|
||||
'member_name' => 'Nom complet du membre',
|
||||
'app_name' => 'Nom de l\'application',
|
||||
],
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
NotificationTemplate::updateOrCreate(
|
||||
['identifier' => 'member_renewal_reminder_2'],
|
||||
[
|
||||
'name' => 'Relance renouvellement — 2ème relance (mi-décembre)',
|
||||
'subject' => 'Pensez à renouveler votre adhésion — {app_name}',
|
||||
'body' => '<p>Bonjour {member_name},</p>'
|
||||
.'<p>Nous vous avons récemment contacté concernant le renouvellement de votre adhésion pour l\'année prochaine.</p>'
|
||||
.'<p>Si vous n\'avez pas encore renouvelé, nous vous encourageons à le faire rapidement pour garantir la continuité de vos services.</p>'
|
||||
.'<p>N\'hésitez pas à nous contacter si vous avez des questions.</p>'
|
||||
.'<p>Cordialement,<br>L\'équipe {app_name}</p>',
|
||||
'variables' => [
|
||||
'member_name' => 'Nom complet du membre',
|
||||
'app_name' => 'Nom de l\'application',
|
||||
],
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
NotificationTemplate::updateOrCreate(
|
||||
['identifier' => 'member_renewal_reminder_3'],
|
||||
[
|
||||
'name' => 'Relance renouvellement — 3ème relance (début janvier)',
|
||||
'subject' => 'Dernière relance : renouvellement de votre adhésion — {app_name}',
|
||||
'body' => '<p>Bonjour {member_name},</p>'
|
||||
.'<p>Nous vous contactons une dernière fois concernant le renouvellement de votre adhésion.</p>'
|
||||
.'<p><strong>Sans action de votre part d\'ici la fin janvier, votre compte et vos services seront automatiquement désactivés.</strong></p>'
|
||||
.'<p>Pour éviter toute interruption, nous vous invitons à renouveler dès maintenant.</p>'
|
||||
.'<p>Pour toute question, n\'hésitez pas à nous contacter.</p>'
|
||||
.'<p>Cordialement,<br>L\'équipe {app_name}</p>',
|
||||
'variables' => [
|
||||
'member_name' => 'Nom complet du membre',
|
||||
'app_name' => 'Nom de l\'application',
|
||||
],
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
NotificationTemplate::updateOrCreate(
|
||||
['identifier' => 'member_account_deactivated'],
|
||||
[
|
||||
'name' => 'Compte désactivé pour non-renouvellement',
|
||||
'subject' => 'Votre compte {app_name} a été désactivé',
|
||||
'body' => '<p>Bonjour {member_name},</p>'
|
||||
.'<p>Faute de renouvellement de votre adhésion, votre compte et vos services ont été désactivés.</p>'
|
||||
.'<p>Si vous souhaitez réactiver votre compte, n\'hésitez pas à nous contacter.</p>'
|
||||
.'<p>Nous vous remercions pour la confiance que vous nous avez accordée.</p>'
|
||||
.'<p>Cordialement,<br>L\'équipe {app_name}</p>',
|
||||
'variables' => [
|
||||
'member_name' => 'Nom complet du membre',
|
||||
'app_name' => 'Nom de l\'application',
|
||||
],
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
NotificationTemplate::updateOrCreate(
|
||||
['identifier' => 'admin_password_reset'],
|
||||
[
|
||||
|
||||
@@ -12,20 +12,20 @@ class PackageSeeder extends Seeder
|
||||
$packages = [
|
||||
[
|
||||
'identifier' => 'custom',
|
||||
'name' => 'Sur-mesure',
|
||||
'description' => 'Calcul du nombre de mois restant dans l\'année',
|
||||
'name' => 'Année en cours',
|
||||
'description' => 'Mois restants dans l\'année civile en cours (1€/mois)',
|
||||
'price' => '1.00',
|
||||
],
|
||||
[
|
||||
'identifier' => 'one-year',
|
||||
'name' => 'Un an',
|
||||
'description' => '12 mois à compter de la date de validation de l\'adhésion du membre',
|
||||
'name' => 'Année en cours + 1 an',
|
||||
'description' => 'Jusqu\'à la fin de l\'année civile suivante (1€/mois)',
|
||||
'price' => '12.00',
|
||||
],
|
||||
[
|
||||
'identifier' => 'two-years',
|
||||
'name' => 'Deux ans',
|
||||
'description' => '24 mois à compter de la date de validation de l\'adhésion du membre',
|
||||
'name' => 'Année en cours + 2 ans',
|
||||
'description' => 'Jusqu\'à la fin de la 2e année civile suivante (1€/mois)',
|
||||
'price' => '24.00',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Deployer;
|
||||
|
||||
require 'recipe/laravel.php';
|
||||
@@ -8,10 +9,10 @@ require 'contrib/rsync.php';
|
||||
$env = $_SERVER['CI_COMMIT_REF_NAME'] === 'master' ? 'PROD' : 'STAGING';
|
||||
|
||||
$envConfig = [
|
||||
'host' => $_SERVER[$env . '_SSH_HOST'],
|
||||
'port' => $_SERVER[$env . '_SSH_PORT'] ?? 22,
|
||||
'user' => $_SERVER[$env . '_SSH_USER'],
|
||||
'path' => $_SERVER[$env . '_SSH_PATH'],
|
||||
'host' => $_SERVER[$env.'_SSH_HOST'],
|
||||
'port' => $_SERVER[$env.'_SSH_PORT'] ?? 22,
|
||||
'user' => $_SERVER[$env.'_SSH_USER'],
|
||||
'path' => $_SERVER[$env.'_SSH_PATH'],
|
||||
'repository' => $_SERVER['CI_REPOSITORY_URL'],
|
||||
];
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ return [
|
||||
'fields' => [
|
||||
'contact' => 'Contact',
|
||||
'contacts' => 'Contacts',
|
||||
],
|
||||
],
|
||||
'responses' => [
|
||||
'success' => 'Your message has been sent successfully',
|
||||
'error' => 'An error occurred while sending the message',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
@@ -9,6 +9,6 @@ return [
|
||||
'description' => 'Description',
|
||||
'created_at' => 'Created at',
|
||||
'Updated at' => 'Updated at',
|
||||
'view_members' => 'View members of this group'
|
||||
'view_members' => 'View members of this group',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -18,6 +18,11 @@ return [
|
||||
'country' => 'Country',
|
||||
'phone1' => 'Phone 1',
|
||||
'phone2' => 'Phone 2',
|
||||
'type_id' => 'Member type',
|
||||
'individual' => 'Individual',
|
||||
'company' => 'Company',
|
||||
'association' => 'Association',
|
||||
'retzien_email' => 'Retzien email',
|
||||
'public_membership' => 'Public membership',
|
||||
'created_at' => 'Created at',
|
||||
'updated_at' => 'Updated at',
|
||||
|
||||
@@ -8,5 +8,5 @@ return [
|
||||
'name' => 'Name',
|
||||
'description' => 'Description',
|
||||
'is_active' => 'Active',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'reset' => 'Your password has been reset.',
|
||||
'sent' => 'We have emailed your password reset link.',
|
||||
'sent_if_exists' => 'A reset link will be sent if the account exists.',
|
||||
'throttled' => 'Please wait before retrying.',
|
||||
'token' => 'This password reset token is invalid.',
|
||||
'user' => 'We can\'t find a user with that email address.',
|
||||
'reset' => 'Your password has been reset.',
|
||||
'sent' => 'We have emailed your password reset link.',
|
||||
'sent_if_exists' => 'A reset link will be sent if the account exists.',
|
||||
'throttled' => 'Please wait before retrying.',
|
||||
'token' => 'This password reset token is invalid.',
|
||||
'user' => 'We can\'t find a user with that email address.',
|
||||
];
|
||||
|
||||
@@ -9,5 +9,5 @@ return [
|
||||
'description' => 'Description',
|
||||
'url' => 'URL',
|
||||
'icon' => 'Icon',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
@@ -4,9 +4,9 @@ return [
|
||||
'fields' => [
|
||||
'contact' => 'un contact',
|
||||
'contacts' => 'Contacts',
|
||||
],
|
||||
],
|
||||
'responses' => [
|
||||
'success' => 'Votre message a été envoyé avec succès. Nous vous recontacterons dans les plus brefs délais.',
|
||||
'error' => 'Une erreur est survenue lors de l\'envoi du message',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
@@ -9,6 +9,6 @@ return [
|
||||
'description' => 'Description',
|
||||
'created_at' => 'Créé le',
|
||||
'updated_at' => 'Mis à jour le',
|
||||
'view_members' => 'Voir les membres de ce groupe'
|
||||
'view_members' => 'Voir les membres de ce groupe',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -28,6 +28,11 @@ return [
|
||||
'country' => 'Pays',
|
||||
'phone1' => 'Téléphone fixe',
|
||||
'phone2' => 'Téléphone portable',
|
||||
'type_id' => 'Type d\'adhérent',
|
||||
'individual' => 'Particulier',
|
||||
'company' => 'Entreprise',
|
||||
'association' => 'Association',
|
||||
'retzien_email' => 'Adresse mail Retzien',
|
||||
'public_membership' => 'Adhésion publique',
|
||||
'created_at' => 'Créé le',
|
||||
'updated_at' => 'Mis à jour le',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'fields' => [
|
||||
'package' => 'Package',
|
||||
'packages' => 'Packages',
|
||||
'identifier' => 'Identifiant',
|
||||
'name' => 'Nom',
|
||||
'description' => 'Description',
|
||||
'is_active' => 'Actif',
|
||||
]
|
||||
'fields' => [
|
||||
'package' => 'Package',
|
||||
'packages' => 'Packages',
|
||||
'identifier' => 'Identifiant',
|
||||
'name' => 'Nom',
|
||||
'description' => 'Description',
|
||||
'is_active' => 'Actif',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'reset' => 'Votre mot de passe a été réinitialisé.',
|
||||
'sent' => 'Nous vous avons envoyé le lien de réinitialisation par e-mail.',
|
||||
'sent_if_exists' => 'Un lien de réinitialisation sera envoyé si le compte existe.',
|
||||
'throttled' => 'Veuillez patienter avant de réessayer.',
|
||||
'token' => 'Ce jeton de réinitialisation est invalide.',
|
||||
'user' => 'Aucun utilisateur trouvé avec cette adresse e-mail.',
|
||||
];
|
||||
'reset' => 'Votre mot de passe a été réinitialisé.',
|
||||
'sent' => 'Nous vous avons envoyé le lien de réinitialisation par e-mail.',
|
||||
'sent_if_exists' => 'Un lien de réinitialisation sera envoyé si le compte existe.',
|
||||
'throttled' => 'Veuillez patienter avant de réessayer.',
|
||||
'token' => 'Ce jeton de réinitialisation est invalide.',
|
||||
'user' => 'Aucun utilisateur trouvé avec cette adresse e-mail.',
|
||||
];
|
||||
|
||||
0
memberships
Normal file
0
memberships
Normal file
1212
package-lock.json
generated
1212
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,7 @@
|
||||
"input-otp": "^1.4.2",
|
||||
"laravel-vite-plugin": "^2.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder'
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -16,7 +16,7 @@ create.definition = {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
create.url = (options?: RouteQueryOptions) => {
|
||||
@@ -25,7 +25,7 @@ create.url = (options?: RouteQueryOptions) => {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -35,7 +35,7 @@ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
@@ -45,7 +45,7 @@ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -55,7 +55,7 @@ const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -65,7 +65,7 @@ createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -82,7 +82,7 @@ create.form = createForm
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||
@@ -97,7 +97,7 @@ store.definition = {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
store.url = (options?: RouteQueryOptions) => {
|
||||
@@ -106,7 +106,7 @@ store.url = (options?: RouteQueryOptions) => {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||
@@ -116,7 +116,7 @@ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||
@@ -126,7 +126,7 @@ const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> =>
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||
|
||||
@@ -6,7 +6,7 @@ export function AboutSection() {
|
||||
<section id="about-section" className="w-full py-16">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<SectionHeading title="Qui sommes-nous ?" color="secondary"
|
||||
subtitle="Le Retzien Libre, c’est une association qui promeut l’auto-hébergement et la décentralisation des services en ligne depuis 2017."
|
||||
subtitle="Le Retzien Libre est une association qui promeut l’auto-hébergement et la décentralisation des services en ligne depuis 2017."
|
||||
align='left'/>
|
||||
<div
|
||||
className="nb-shadow-static bg-white rounded-4xl mt-10 px-10 pt-20 pb-10">
|
||||
@@ -14,14 +14,14 @@ export function AboutSection() {
|
||||
<div className="flex flex-col gap-3 lg:border-r-2 border-black lg:pr-10 border-0">
|
||||
<h3 className="text-xl text-primary font-semibold">Une association locale</h3>
|
||||
<p>
|
||||
Nous voulons vous proposer à travers des outils libres, ouverts et solidaires, de
|
||||
Nous vous proposons, à travers des outils libres, ouverts et solidaires, de
|
||||
quitter l'industrie du G.A.F.A.M.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 border-r-2 border-black pr-10">
|
||||
<h3 className="text-xl text-primary font-semibold">Notre mission</h3>
|
||||
<p>
|
||||
Nous nous positionnons comme "A.M.A.P. informatique", délivrant des services "bio",
|
||||
Nous sommes comme une "A.M.A.P. informatique", délivrant des services "bio",
|
||||
"éthiques" et "locaux" dans le Pays de Retz et Nantes.
|
||||
</p>
|
||||
</div>
|
||||
@@ -34,7 +34,12 @@ export function AboutSection() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex justify-center mt-20">
|
||||
<Button variant="default">En savoir plus en lisant notre Blog</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => window.open('https://blog.retzien.fr/index.php', '_blank')}
|
||||
>
|
||||
En savoir plus en lisant notre Blog
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,8 +23,8 @@ export function AlternativeSection() {
|
||||
Notre alternative : Le Retzien Libre
|
||||
</h2>
|
||||
<p>
|
||||
Ici, pas d’exploitation des vos données personnelles à des fins commerciales, ni de dépense aux services centralisés.<br/>
|
||||
Le Retzien Libre, c’est une association locale engagée pour la promotion du logiciel libre et la protection de vos données personnelles.
|
||||
Ici, pas d’exploitation des vos données personnelles à des fins commerciales, ni de dépendance aux services centralisés.<br/>
|
||||
Le Retzien Libre est une association locale engagée pour la promotion du logiciel libre et la protection de vos données personnelles.
|
||||
</p>
|
||||
{auth.user ? (
|
||||
<Link href={dashboard()}>
|
||||
|
||||
@@ -53,7 +53,7 @@ const services: Service[] = [
|
||||
},
|
||||
{
|
||||
title: 'Partage de fichiers',
|
||||
description: 'Partager facielement vos fichiers en toute sécurité',
|
||||
description: 'Partager facilement vos fichiers en toute sécurité',
|
||||
colorTitle: 'black',
|
||||
bgTitle: 'white',
|
||||
bgColor: 'primary',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Link} from '@inertiajs/react';
|
||||
import {contact, home, membership} from '@/routes';
|
||||
import {contact, faq, home, membership} from '@/routes';
|
||||
import AppLogoIcon from '@/components/app-logo-icon';
|
||||
import LogoChaton from '@/img/utils/logo-chaton.png';
|
||||
|
||||
@@ -32,6 +32,8 @@ export function Footer() {
|
||||
<Link href={home()} className="text-sm text-white no-underline hover:underline">Accueil</Link>
|
||||
<Link href={contact()} className="text-sm text-white no-underline hover:underline">Contact</Link>
|
||||
<Link href={membership()} className="text-sm text-white no-underline hover:underline">Adhérer</Link>
|
||||
<Link href={faq()} className="text-sm text-white no-underline hover:underline">FAQ</Link>
|
||||
<a href="https://doc.retzien.fr" target="_blank" rel="noopener noreferrer" className="text-sm text-white no-underline hover:underline">Documentation</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between gap-4 border-t border-black/20 pt-6 text-sm">
|
||||
|
||||
64
resources/js/components/ui/accordion.tsx
Normal file
64
resources/js/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as React from "react"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { Accordion as AccordionPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"flex flex-1 items-start justify-between gap-4 rounded-md border-0 shadow-none py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -34,7 +34,7 @@ function RadioGroupItem({
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary stroke-0 absolute top-1/2 -left-2 size-5 -translate-x-1/2 -translate-y-1/2" />
|
||||
<CircleIcon className="fill-secondary stroke-0 absolute top-1/2 -left-2 size-5 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
|
||||
@@ -66,9 +66,9 @@ export default function NavGuestLayout() {
|
||||
<Link href="#services" className="px-5 py-1.5 text-lg text-[#1b1b18] dark:text-[#EDEDEC] no-underline hover:underline">
|
||||
Nos Services
|
||||
</Link>
|
||||
<Link href="#" className="px-5 py-1.5 text-lg text-[#1b1b18] dark:text-[#EDEDEC] no-underline hover:underline">
|
||||
<a href="https://blog.retzien.fr/index.php" target="_blank" rel="noopener noreferrer" className="px-5 py-1.5 text-lg text-[#1b1b18] dark:text-[#EDEDEC] no-underline hover:underline">
|
||||
Le Blog
|
||||
</Link>
|
||||
</a>
|
||||
<Link href={contact()} className="px-5 py-1.5 text-lg text-[#1b1b18] dark:text-[#EDEDEC] no-underline hover:underline">
|
||||
Contact
|
||||
</Link>
|
||||
@@ -165,9 +165,9 @@ export default function NavGuestLayout() {
|
||||
<Link href="#services" onClick={closeMenu} className="text-lg py-3 border-b border-black/10 dark:border-white/10 no-underline">
|
||||
<span className="text-black hover:underline">Nos Services</span>
|
||||
</Link>
|
||||
<Link href="#" onClick={closeMenu} className="text-lg py-3 border-b border-black/10 dark:border-white/10 no-underline">
|
||||
<a href="https://blog.retzien.fr/index.php" target="_blank" onClick={closeMenu} className="text-lg py-3 border-b border-black/10 dark:border-white/10 no-underline">
|
||||
<span className="text-black hover:underline">Le Blog</span>
|
||||
</Link>
|
||||
</a>
|
||||
<Link href={contact()} onClick={closeMenu} className="text-lg py-3 no-underline">
|
||||
<span className="text-black hover:underline">Contact</span>
|
||||
</Link>
|
||||
|
||||
112
resources/js/pages/faq.tsx
Normal file
112
resources/js/pages/faq.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Head } from '@inertiajs/react';
|
||||
import NavGuestLayout from '@/layouts/nav-guest-layout';
|
||||
import { Footer } from '@/components/footer';
|
||||
import { Container } from '@/components/common/Container';
|
||||
import { SectionHeading } from '@/components/common/SectionHeading';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
question: "Qu'est-ce que Le Retzien Libre ?",
|
||||
answer: "Le Retzien Libre est une association locale engagée pour un internet éthique, libre et respectueux de la vie privée. Nous proposons des services numériques basés sur des logiciels libres, hébergés localement, en alternative aux géants du numérique.",
|
||||
},
|
||||
{
|
||||
question: "Comment adhérer à l'association ?",
|
||||
answer: "Vous pouvez adhérer directement en ligne via notre formulaire d'adhésion. Choisissez votre formule (année en cours, 1 an ou 2 ans), remplissez vos informations et validez. Notre équipe traitera votre demande dans les meilleurs délais.",
|
||||
},
|
||||
{
|
||||
question: "Quels services sont inclus dans l'adhésion ?",
|
||||
answer: "L'adhésion inclut l'accès à l'ensemble de nos services : boîte mail @retzien.fr, stockage cloud via NextCloud, hébergement web, listes de diffusion et bien plus. Consultez notre documentation pour la liste complète des services disponibles.",
|
||||
},
|
||||
{
|
||||
question: "Combien coûte l'adhésion ?",
|
||||
answer: "L'adhésion est à 1€ par mois. Le montant est calculé en fonction de la durée choisie : reste de l'année en cours, année en cours + 1 an, ou année en cours + 2 ans. Les adhésions sont basées sur l'année civile (janvier à décembre).",
|
||||
},
|
||||
{
|
||||
question: "Comment fonctionne ma boîte mail @retzien.fr ?",
|
||||
answer: "Lors de votre adhésion, vous pouvez indiquer le nom de boîte mail souhaité (ex : prenom.nom@retzien.fr). Une fois votre adhésion validée, votre boîte sera créée et vous recevrez vos identifiants de connexion. Vous pouvez la consulter via webmail ou la configurer dans votre client mail préféré.",
|
||||
},
|
||||
{
|
||||
question: "Que se passe-t-il à l'expiration de mon adhésion ?",
|
||||
answer: "Vous recevrez un rappel par e-mail avant l'expiration de votre adhésion. Si vous ne renouvelez pas, vos services seront suspendus. Vos données sont conservées pendant une période raisonnable pour vous permettre de renouveler sans perte.",
|
||||
},
|
||||
{
|
||||
question: "Comment contacter l'association ?",
|
||||
answer: "Vous pouvez nous joindre via le formulaire de contact disponible sur le site, ou directement par e-mail. Pour les questions techniques, consultez d'abord notre documentation en ligne.",
|
||||
},
|
||||
{
|
||||
question: "Où trouver la documentation technique ?",
|
||||
answer: "Notre documentation complète est disponible à l'adresse doc.retzien.fr. Vous y trouverez des guides de configuration pour tous nos services, des tutoriels et une base de connaissances.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Faq() {
|
||||
return (
|
||||
<>
|
||||
<Head title="Foire aux questions" />
|
||||
<div className="flex flex-col min-h-screen bg-white dark:bg-[#0a0a0a] text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||
<div className="flex flex-col items-center px-4">
|
||||
<NavGuestLayout />
|
||||
</div>
|
||||
|
||||
<main className="flex-1 py-12">
|
||||
<Container className="flex flex-col gap-10 max-w-4xl mx-auto">
|
||||
<SectionHeading
|
||||
title="Foire aux questions"
|
||||
color="primary"
|
||||
subtitle="Retrouvez les réponses aux questions les plus fréquentes sur notre association et nos services."
|
||||
align="left"
|
||||
/>
|
||||
|
||||
<Accordion type="single" collapsible className="w-full flex flex-col gap-3">
|
||||
{faqItems.map((item, index) => (
|
||||
<AccordionItem
|
||||
key={index}
|
||||
value={`item-${index}`}
|
||||
className="nb-shadow-static rounded-2xl border-3 border-black px-5 overflow-hidden"
|
||||
>
|
||||
<AccordionTrigger className="text-left font-semibold text-base py-4 hover:no-underline">
|
||||
{item.question}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground pb-4">
|
||||
{item.answer}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
|
||||
<div className="bg-primary dark:bg-[#171717] nb-shadow-static rounded-2xl p-6 text-center">
|
||||
<p className="text-accent font-semibold mb-2">Vous n'avez pas trouvé votre réponse ?</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Consultez notre documentation complète ou contactez-nous directement.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<a
|
||||
href="https://doc.retzien.fr"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="nb-shadow inline-flex items-center justify-center rounded-md text-sm font-bold bg-secondary text-secondary-foreground hover:bg-secondary/80 h-10 px-4 py-2 no-underline"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
href="/contact"
|
||||
className="nb-shadow inline-flex items-center justify-center rounded-md text-sm font-bold bg-accent text-white hover:bg-accent/80 h-10 px-4 py-2 no-underline"
|
||||
>
|
||||
Nous contacter
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -54,6 +54,14 @@ export default function Contact() {
|
||||
className="mx-auto"
|
||||
/>
|
||||
|
||||
<p className="text-sm text-muted-foreground max-w-4xl mx-auto">
|
||||
Pensez à consulter notre{' '}
|
||||
<a href="/faq" className="text-primary underline hover:no-underline">FAQ</a>
|
||||
{' '}ou notre{' '}
|
||||
<a href="https://doc.retzien.fr" target="_blank" rel="noopener noreferrer" className="text-primary underline hover:no-underline">documentation</a>
|
||||
{' '}avant de nous écrire.
|
||||
</p>
|
||||
|
||||
{showFlashMessage && <FlashMessage messages={flash ?? {}} />}
|
||||
|
||||
<Form
|
||||
|
||||
@@ -8,6 +8,7 @@ import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import NavGuestLayout from '@/layouts/nav-guest-layout';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PageProps } from '@/types';
|
||||
import { FlashMessage } from '@/components/flash-message';
|
||||
@@ -17,12 +18,14 @@ import { Footer } from '@/components/footer';
|
||||
import IllustrationLogo from "@/img/utils/lrl-logo-full.svg";
|
||||
|
||||
export default function Membership() {
|
||||
const { flash, plans, services, captcha_question } = usePage().props as PageProps;
|
||||
const { flash, plans, services, member_types, current_year, captcha_question } = usePage().props as PageProps;
|
||||
const [showFlashMessage, setFlashMessage] = useState(!!flash);
|
||||
const [selectedPlan, setSelectedPlan] = useState(plans?.[0]?.identifier ?? null);
|
||||
const [amount, setAmount] = useState(plans?.[0]?.price ?? 0);
|
||||
const [memberType, setMemberType] = useState(member_types?.[0]?.identifier ?? 'individual');
|
||||
const [phone1Value, setPhone1Value] = useState('');
|
||||
const [zipcodeValue, setZipcodeValue] = useState('');
|
||||
const [retzienEmailValue, setRetzienEmailValue] = useState('');
|
||||
|
||||
const phone1LocalError = phone1Value.length > 0 && !/^\d{10}$/.test(phone1Value)
|
||||
? 'Le numéro doit contenir exactement 10 chiffres.'
|
||||
@@ -32,6 +35,10 @@ export default function Membership() {
|
||||
? 'Le code postal doit contenir exactement 5 chiffres.'
|
||||
: null;
|
||||
|
||||
const retzienEmailLocalError = retzienEmailValue.length > 0 && !/^[a-z0-9._-]+$/.test(retzienEmailValue)
|
||||
? 'Caractères autorisés : lettres minuscules, chiffres, points, tirets.'
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (plans && selectedPlan) {
|
||||
const plan = plans.find((p) => p.identifier === selectedPlan);
|
||||
@@ -47,6 +54,20 @@ export default function Membership() {
|
||||
}
|
||||
}, [flash]);
|
||||
|
||||
const getPlanLabel = (plan: NonNullable<typeof plans>[number]) => {
|
||||
if (!current_year || !plan.months) return plan.name;
|
||||
if (plan.identifier === 'custom') {
|
||||
return `Année ${current_year} (${plan.months} mois restants)`;
|
||||
}
|
||||
if (plan.identifier === 'one-year') {
|
||||
return `Années ${current_year}–${current_year + 1}`;
|
||||
}
|
||||
if (plan.identifier === 'two-years') {
|
||||
return `Années ${current_year}–${current_year + 2}`;
|
||||
}
|
||||
return plan.name;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Adhérer au Retzien Libre" />
|
||||
@@ -85,6 +106,28 @@ export default function Membership() {
|
||||
className="rounded-lg max-w-md w-full pt-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Member type radio */}
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-accent font-semibold">Vous êtes*</Label>
|
||||
<RadioGroup
|
||||
value={memberType}
|
||||
onValueChange={setMemberType}
|
||||
className="flex flex-wrap gap-4"
|
||||
>
|
||||
{member_types?.map((type) => (
|
||||
<div key={type.identifier} className="flex items-center gap-2">
|
||||
<RadioGroupItem value={type.identifier} id={`member-type-${type.identifier}`} />
|
||||
<Label htmlFor={`member-type-${type.identifier}`} className="cursor-pointer">
|
||||
{type.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<input type="hidden" name="member_type" value={memberType} />
|
||||
<InputError message={errors.member_type} />
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg font-semibold text-accent">Vos informations</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
@@ -130,9 +173,32 @@ export default function Membership() {
|
||||
<InputError message={errors.company} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="desired_retzien_email">
|
||||
Adresse mail Retzien souhaitée <span className="text-muted-foreground text-xs">(facultatif)</span>
|
||||
</Label>
|
||||
<div className="group/email flex items-center gap-0 rounded-md has-[:focus-visible]:shadow-[4px_4px_0px_rgba(0,0,0,1)] transition duration-100 ease-in-out">
|
||||
<Input
|
||||
id="desired_retzien_email"
|
||||
name="desired_retzien_email"
|
||||
type="text"
|
||||
tabIndex={6}
|
||||
autoComplete="off"
|
||||
placeholder="prenom.nom"
|
||||
className="rounded-r-none focus-visible:shadow-none"
|
||||
value={retzienEmailValue}
|
||||
onChange={(e) => setRetzienEmailValue(e.target.value.toLowerCase().replace(/[^a-z0-9._-]/g, ''))}
|
||||
/>
|
||||
<span className="inline-flex items-center px-3 h-9 border-3 border-l-0 border-black rounded-r-md bg-muted text-sm text-muted-foreground whitespace-nowrap">
|
||||
@retzien.fr
|
||||
</span>
|
||||
</div>
|
||||
<InputError message={retzienEmailLocalError ?? errors.desired_retzien_email} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="address">Adresse*</Label>
|
||||
<Input id="address" name="address" type="text" required tabIndex={6} autoComplete="street-address" placeholder="Votre adresse" />
|
||||
<Input id="address" name="address" type="text" required tabIndex={7} autoComplete="street-address" placeholder="Votre adresse" />
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
|
||||
@@ -144,7 +210,7 @@ export default function Membership() {
|
||||
name="zipcode"
|
||||
type="text"
|
||||
required
|
||||
tabIndex={7}
|
||||
tabIndex={8}
|
||||
autoComplete="postal-code"
|
||||
inputMode="numeric"
|
||||
maxLength={5}
|
||||
@@ -156,7 +222,7 @@ export default function Membership() {
|
||||
</div>
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="city">Ville*</Label>
|
||||
<Input id="city" name="city" type="text" required tabIndex={8} autoComplete="address-level2" placeholder="Ville" />
|
||||
<Input id="city" name="city" type="text" required tabIndex={9} autoComplete="address-level2" placeholder="Ville" />
|
||||
<InputError message={errors.city} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,7 +240,7 @@ export default function Membership() {
|
||||
<button
|
||||
key={plan.id}
|
||||
type="button"
|
||||
tabIndex={9}
|
||||
tabIndex={10}
|
||||
onClick={() => setSelectedPlan(plan.identifier)}
|
||||
className={cn(
|
||||
'flex items-center justify-between rounded-xl border-3 border-black px-5 py-4 text-left transition-all duration-150',
|
||||
@@ -185,14 +251,12 @@ export default function Membership() {
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-base">{plan.name}</span>
|
||||
{plan.months != null ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{plan.months} mois × 1€/mois
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">{plan.description}</span>
|
||||
)}
|
||||
<span className="font-bold text-base">{getPlanLabel(plan)}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{plan.months != null
|
||||
? `${plan.months} mois × 1€/mois`
|
||||
: plan.description}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-2xl font-black">{plan.price}€</span>
|
||||
</button>
|
||||
@@ -204,7 +268,7 @@ export default function Membership() {
|
||||
<InputError message={errors.package} />
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground border-t border-border pt-3">
|
||||
Montant total : <strong className="text-secondary text-lg">{amount}€</strong>
|
||||
Montant total : <strong className="text-accent text-2xl">{amount}€</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -222,6 +286,9 @@ export default function Membership() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
Et plus encore ...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Captcha + CGU + Submit */}
|
||||
@@ -232,7 +299,7 @@ export default function Membership() {
|
||||
id="captcha"
|
||||
name="captcha"
|
||||
type="text"
|
||||
tabIndex={10}
|
||||
tabIndex={11}
|
||||
placeholder="Votre réponse"
|
||||
autoComplete="off"
|
||||
className="max-w-[180px]"
|
||||
@@ -242,7 +309,7 @@ export default function Membership() {
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox id="cgu" name="cgu" tabIndex={11} required className="mt-0.5" />
|
||||
<Checkbox id="cgu" name="cgu" tabIndex={12} required className="mt-0.5" />
|
||||
<Label htmlFor="cgu" className="text-sm leading-snug cursor-pointer">
|
||||
J'ai lu et j'accepte les <a href="#">C.G.U.</a> et je comprends la
|
||||
nécessité des enregistrements de mes données personnelles.
|
||||
@@ -254,7 +321,7 @@ export default function Membership() {
|
||||
<Button
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
tabIndex={12}
|
||||
tabIndex={13}
|
||||
className="nb-shadow w-full font-bold text-base py-5"
|
||||
>
|
||||
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||
|
||||
@@ -365,6 +365,80 @@ dashboardForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =
|
||||
|
||||
dashboard.form = dashboardForm
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
export const faq = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
url: faq.url(options),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
faq.definition = {
|
||||
methods: ["get","head"],
|
||||
url: '/faq',
|
||||
} satisfies RouteDefinition<["get","head"]>
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faq.url = (options?: RouteQueryOptions) => {
|
||||
return faq.definition.url + queryParams(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faq.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
url: faq.url(options),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faq.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
url: faq.url(options),
|
||||
method: 'head',
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
const faqForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
action: faq.url(options),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faqForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
action: faq.url(options),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @route '/faq'
|
||||
*/
|
||||
faqForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
action: faq.url({
|
||||
[options?.mergeQuery ? 'mergeQuery' : 'query']: {
|
||||
_method: 'HEAD',
|
||||
...(options?.query ?? options?.mergeQuery ?? {}),
|
||||
}
|
||||
}),
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
faq.form = faqForm
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Auth\RegisteredUserController::register
|
||||
* @see app/Http/Controllers/Auth/RegisteredUserController.php:21
|
||||
@@ -529,7 +603,7 @@ contact.form = contactForm
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
export const membership = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -544,7 +618,7 @@ membership.definition = {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membership.url = (options?: RouteQueryOptions) => {
|
||||
@@ -553,7 +627,7 @@ membership.url = (options?: RouteQueryOptions) => {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -563,7 +637,7 @@ membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
@@ -573,7 +647,7 @@ membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -583,7 +657,7 @@ const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'>
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -593,7 +667,7 @@ membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:19
|
||||
* @route '/membership'
|
||||
*/
|
||||
membershipForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../wayfinder'
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
export const mentions = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -14,7 +14,7 @@ mentions.definition = {
|
||||
} satisfies RouteDefinition<["get","head"]>
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentions.url = (options?: RouteQueryOptions) => {
|
||||
@@ -22,7 +22,7 @@ mentions.url = (options?: RouteQueryOptions) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentions.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -31,7 +31,7 @@ mentions.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentions.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
@@ -40,7 +40,7 @@ mentions.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
const mentionsForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -49,7 +49,7 @@ const mentionsForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentionsForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -58,7 +58,7 @@ mentionsForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:20
|
||||
* @see routes/web.php:21
|
||||
* @route '/mentions-legales'
|
||||
*/
|
||||
mentionsForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -74,7 +74,7 @@ mentionsForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
|
||||
mentions.form = mentionsForm
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
export const cgu = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -88,7 +88,7 @@ cgu.definition = {
|
||||
} satisfies RouteDefinition<["get","head"]>
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cgu.url = (options?: RouteQueryOptions) => {
|
||||
@@ -96,7 +96,7 @@ cgu.url = (options?: RouteQueryOptions) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cgu.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -105,7 +105,7 @@ cgu.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cgu.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
@@ -114,7 +114,7 @@ cgu.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
const cguForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -123,7 +123,7 @@ const cguForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cguForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -132,7 +132,7 @@ cguForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:21
|
||||
* @see routes/web.php:22
|
||||
* @route '/conditions-generales'
|
||||
*/
|
||||
cguForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -148,7 +148,7 @@ cguForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
cgu.form = cguForm
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
export const confidentialite = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -162,7 +162,7 @@ confidentialite.definition = {
|
||||
} satisfies RouteDefinition<["get","head"]>
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialite.url = (options?: RouteQueryOptions) => {
|
||||
@@ -170,7 +170,7 @@ confidentialite.url = (options?: RouteQueryOptions) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialite.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||
@@ -179,7 +179,7 @@ confidentialite.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => (
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialite.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||
@@ -188,7 +188,7 @@ confidentialite.head = (options?: RouteQueryOptions): RouteDefinition<'head'> =>
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
const confidentialiteForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -197,7 +197,7 @@ const confidentialiteForm = (options?: RouteQueryOptions): RouteFormDefinition<'
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialiteForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
@@ -206,7 +206,7 @@ confidentialiteForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'ge
|
||||
})
|
||||
|
||||
/**
|
||||
* @see routes/web.php:22
|
||||
* @see routes/web.php:23
|
||||
* @route '/confidentialite'
|
||||
*/
|
||||
confidentialiteForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../wayfinder'
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||
@@ -16,7 +16,7 @@ store.definition = {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
store.url = (options?: RouteQueryOptions) => {
|
||||
@@ -25,7 +25,7 @@ store.url = (options?: RouteQueryOptions) => {
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||
@@ -35,7 +35,7 @@ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||
@@ -45,7 +45,7 @@ const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> =>
|
||||
|
||||
/**
|
||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:59
|
||||
* @route '/membership'
|
||||
*/
|
||||
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||
|
||||
8
resources/js/types/index.d.ts
vendored
8
resources/js/types/index.d.ts
vendored
@@ -58,6 +58,12 @@ export interface Plans {
|
||||
months?: number | null;
|
||||
}
|
||||
|
||||
export interface MemberTypeOption {
|
||||
id: number;
|
||||
identifier: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface MembershipService {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -114,6 +120,8 @@ export interface PageProps {
|
||||
auth?: Auth;
|
||||
plans?: Plans[];
|
||||
services?: MembershipService[];
|
||||
member_types?: MemberTypeOption[];
|
||||
current_year?: number;
|
||||
captcha_question?: string;
|
||||
member?: DashboardMember | null;
|
||||
|
||||
|
||||
@@ -2,7 +2,24 @@
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::command('members:renewal-process --phase=reminder_1')
|
||||
->yearlyOn(12, 1, '09:00')
|
||||
->description('1ère relance renouvellement adhésion');
|
||||
|
||||
Schedule::command('members:renewal-process --phase=reminder_2')
|
||||
->yearlyOn(12, 15, '09:00')
|
||||
->description('2ème relance renouvellement adhésion');
|
||||
|
||||
Schedule::command('members:renewal-process --phase=reminder_3')
|
||||
->yearlyOn(1, 5, '09:00')
|
||||
->description('3ème relance renouvellement adhésion');
|
||||
|
||||
Schedule::command('members:renewal-process --phase=deactivate')
|
||||
->yearlyOn(1, 28, '09:00')
|
||||
->description('Désactivation comptes non renouvelés');
|
||||
|
||||
@@ -17,6 +17,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::post('dashboard/service-activation', [DashboardController::class, 'requestServiceActivation'])->name('dashboard.service-activation');
|
||||
});
|
||||
|
||||
Route::get('/faq', fn () => Inertia::render('faq'))->name('faq');
|
||||
Route::get('/mentions-legales', fn () => Inertia::render('legal/mentions-legales'))->name('legal.mentions');
|
||||
Route::get('/conditions-generales', fn () => Inertia::render('legal/cgu'))->name('legal.cgu');
|
||||
Route::get('/confidentialite', fn () => Inertia::render('legal/confidentialite'))->name('legal.confidentialite');
|
||||
|
||||
68
tests/Feature/Commands/CreateISPWebAccountsTest.php
Normal file
68
tests/Feature/Commands/CreateISPWebAccountsTest.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Commands;
|
||||
|
||||
use App\Enums\IspconfigType;
|
||||
use App\Models\IspconfigMember;
|
||||
use App\Models\Member;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CreateISPWebAccountsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_command_exits_successfully_when_no_members_with_websites(): void
|
||||
{
|
||||
$this->artisan('ext:create-isp-accounts')
|
||||
->expectsOutput('No members with websites found')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
public function test_command_processes_members_regardless_of_status(): void
|
||||
{
|
||||
Member::factory()->create([
|
||||
'status' => 'draft',
|
||||
'website_url' => 'example.com',
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'Doe',
|
||||
]);
|
||||
|
||||
$this->artisan('ext:create-isp-accounts --dry-run')
|
||||
->expectsOutput('DRY RUN MODE - No changes will be made')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
public function test_command_runs_in_dry_run_mode(): void
|
||||
{
|
||||
Member::factory()->create([
|
||||
'status' => 'valid',
|
||||
'website_url' => 'example.com',
|
||||
'firstname' => 'John',
|
||||
'lastname' => 'Doe',
|
||||
]);
|
||||
|
||||
$this->artisan('ext:create-isp-accounts --dry-run')
|
||||
->expectsOutput('DRY RUN MODE - No changes will be made')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
public function test_command_skips_member_with_existing_client(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'status' => 'valid',
|
||||
'website_url' => 'example.com',
|
||||
]);
|
||||
|
||||
IspconfigMember::create([
|
||||
'member_id' => $member->id,
|
||||
'type' => IspconfigType::WEB,
|
||||
'ispconfig_client_id' => '123',
|
||||
]);
|
||||
|
||||
$this->artisan('ext:create-isp-accounts --dry-run')
|
||||
->assertExitCode(0);
|
||||
|
||||
$this->assertDatabaseCount('ispconfigs_members', 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user