gestion addin
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Admin\Concerns\ScopedToTenant;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\EditableField;
|
||||
use App\Models\UserOverride;
|
||||
use App\Signature\ProfileService;
|
||||
use App\Signature\VariableMap;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/** Choix des champs que les agents peuvent modifier eux-mêmes (add-in et portail). */
|
||||
class EditableFieldController extends Controller
|
||||
{
|
||||
use ScopedToTenant;
|
||||
|
||||
/** L'adresse e-mail n'est jamais modifiable : elle doit rester celle de la boîte. */
|
||||
private const EXCLUDED = ['email'];
|
||||
|
||||
public function index(): View
|
||||
{
|
||||
$tenant = $this->tenantId();
|
||||
foreach (array_diff(array_keys(VariableMap::FIELDS), self::EXCLUDED) as $key) {
|
||||
EditableField::firstOrCreate(
|
||||
['tenant_id' => $tenant, 'field' => $key],
|
||||
['label' => VariableMap::FIELDS[$key]['label'], 'enabled' => false, 'validation_rule' => in_array($key, ['telephone', 'mobile'], true) ? 'phone' : 'text'],
|
||||
);
|
||||
}
|
||||
|
||||
$usage = UserOverride::query()
|
||||
->join('directory_users', 'directory_users.id', '=', 'user_overrides.directory_user_id')
|
||||
->where('directory_users.tenant_id', $tenant)
|
||||
->selectRaw('user_overrides.field, count(*) as n')->groupBy('user_overrides.field')->pluck('n', 'field');
|
||||
|
||||
return view('admin.fields.index', [
|
||||
'fields' => EditableField::where('tenant_id', $tenant)->whereNotIn('field', self::EXCLUDED)
|
||||
->get()->sortBy(fn ($f) => array_search($f->field, array_keys(VariableMap::FIELDS)))->values(),
|
||||
'types' => ProfileService::TYPES,
|
||||
'usage' => $usage,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$tenant = $this->tenantId();
|
||||
$data = $request->validate([
|
||||
'fields' => ['array'],
|
||||
'fields.*.enabled' => ['nullable', 'boolean'],
|
||||
'fields.*.label' => ['required', 'string', 'max:60'],
|
||||
'fields.*.type' => ['required', Rule::in(array_keys(ProfileService::TYPES))],
|
||||
]);
|
||||
|
||||
$changes = [];
|
||||
foreach ($data['fields'] ?? [] as $key => $row) {
|
||||
$field = EditableField::where('tenant_id', $tenant)->where('field', $key)->whereNotIn('field', self::EXCLUDED)->first();
|
||||
if (! $field) {
|
||||
continue;
|
||||
}
|
||||
$field->fill(['enabled' => (bool) ($row['enabled'] ?? false), 'label' => $row['label'], 'validation_rule' => $row['type']]);
|
||||
if ($field->isDirty()) {
|
||||
$changes[$key] = $field->getDirty();
|
||||
$field->save();
|
||||
}
|
||||
}
|
||||
if ($changes) {
|
||||
AuditLog::record('admin', $this->actor(), 'fields.update', null, $changes, $tenant);
|
||||
}
|
||||
|
||||
return back()->with('status', $changes ? 'Champs modifiables enregistrés : effet immédiat dans l\'add-in et le portail.' : 'Aucun changement.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Signature\ProfileService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/** Profil de l'agent pour le volet de l'add-in (jeton Entra NAA). */
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function show(Request $request, ProfileService $profiles): JsonResponse
|
||||
{
|
||||
return response()->json($profiles->profile($request->attributes->get('directory_user')))
|
||||
->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT { fields: { mobile: "06…" | null, … }, hidden: ["bureau"], reply_variant?: short|full|none|null }
|
||||
* null = revenir à l'annuaire ; « hidden » = ne pas afficher le champ (les chaînes vides
|
||||
* étant converties en null par Laravel, le masquage passe par cette liste explicite).
|
||||
*/
|
||||
public function update(Request $request, ProfileService $profiles): JsonResponse
|
||||
{
|
||||
$user = $request->attributes->get('directory_user');
|
||||
$fields = $request->input('fields', []);
|
||||
$hidden = $request->input('hidden', []);
|
||||
abort_unless(is_array($fields) && is_array($hidden), 422, 'fields doit être un objet et hidden une liste.');
|
||||
foreach ($hidden as $key) {
|
||||
if (is_string($key)) {
|
||||
$fields[$key] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$profiles->update(
|
||||
$user,
|
||||
$fields,
|
||||
$request->input('reply_variant'),
|
||||
$request->exists('reply_variant'),
|
||||
(string) ($user->mail ?? $user->user_principal_name),
|
||||
);
|
||||
|
||||
return response()->json($profiles->profile($user->fresh()));
|
||||
}
|
||||
}
|
||||
@@ -4,22 +4,53 @@ namespace App\Http\Controllers\Portal;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DirectoryUser;
|
||||
use App\Signature\ProfileService;
|
||||
use App\Signature\SignatureBuilder;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/** Portail agent : aperçu de la signature. Champs modifiables et préférences arrivent au lot 2. */
|
||||
/** Portail agent : aperçu de la signature et modification des champs ouverts par l'admin. */
|
||||
class PortalController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, SignatureBuilder $builder): View
|
||||
public function show(Request $request, SignatureBuilder $builder, ProfileService $profiles): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$agent = DirectoryUser::where('tenant_id', $user->tenant_id)->where('entra_oid', $user->entra_oid)->firstOrFail();
|
||||
$agent = $this->agent($request);
|
||||
|
||||
return view('portal.home', [
|
||||
'agent' => $agent,
|
||||
'profile' => $profiles->profile($agent),
|
||||
'full' => $builder->build($agent, 'new'),
|
||||
'reply' => $builder->build($agent, 'reply'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(Request $request, ProfileService $profiles): RedirectResponse
|
||||
{
|
||||
$agent = $this->agent($request);
|
||||
$values = [];
|
||||
foreach ((array) $request->input('fields', []) as $key => $row) {
|
||||
$mode = $row['mode'] ?? 'directory';
|
||||
$values[$key] = match ($mode) {
|
||||
'custom' => (string) ($row['value'] ?? ''),
|
||||
'hidden' => '',
|
||||
default => null,
|
||||
};
|
||||
if ($mode === 'custom' && trim((string) ($row['value'] ?? '')) === '') {
|
||||
$values[$key] = null; // valeur perso vide : on revient à l'annuaire
|
||||
}
|
||||
}
|
||||
$reply = $request->input('reply_variant');
|
||||
|
||||
$profiles->update($agent, $values, $reply === 'default' ? null : $reply, $request->has('reply_variant'), (string) $agent->mail);
|
||||
|
||||
return back()->with('status', 'Vos informations sont enregistrées : elles s\'appliquent à vos prochains messages.');
|
||||
}
|
||||
|
||||
private function agent(Request $request): DirectoryUser
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
return DirectoryUser::where('tenant_id', $user->tenant_id)->where('entra_oid', $user->entra_oid)->firstOrFail();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Signature;
|
||||
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\DirectoryUser;
|
||||
use App\Models\EditableField;
|
||||
use App\Models\UserOverride;
|
||||
use App\Models\UserPreference;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
* Informations modifiables par l'agent lui-même (add-in et portail).
|
||||
* - Seuls les champs ouverts par l'admin sont acceptés.
|
||||
* - Une surcharge ne modifie que la signature : l'annuaire (AD / Entra) reste la référence.
|
||||
* - Valeur null = revenir à la valeur de l'annuaire ; chaîne vide = ne pas afficher ce champ.
|
||||
* - Publication immédiate, chaque modification est tracée dans le journal d'audit.
|
||||
*/
|
||||
final class ProfileService
|
||||
{
|
||||
public const TYPES = [
|
||||
'text' => ['label' => 'Texte libre', 'rule' => ['string', 'max:120']],
|
||||
'phone' => ['label' => 'Téléphone', 'rule' => ['string', 'max:25', 'regex:/^[0-9 +().\-]{6,25}$/']],
|
||||
];
|
||||
|
||||
/** @return array{fields: array<int, array>, reply_variant: ?string, reply_variant_default: string} */
|
||||
public function profile(DirectoryUser $user): array
|
||||
{
|
||||
$editable = $this->editableFields($user);
|
||||
$overrides = $user->overrides()->pluck('value', 'field');
|
||||
|
||||
$fields = [];
|
||||
foreach ($editable as $f) {
|
||||
$column = VariableMap::FIELDS[$f->field]['column'] ?? null;
|
||||
if (! $column) {
|
||||
continue;
|
||||
}
|
||||
$fields[] = [
|
||||
'key' => $f->field,
|
||||
'label' => $f->label,
|
||||
'type' => $f->validation_rule ?: 'text',
|
||||
'directory_value' => $user->getAttribute($column),
|
||||
'override' => $overrides->has($f->field) ? $overrides[$f->field] : null,
|
||||
'overridden' => $overrides->has($f->field),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'fields' => $fields,
|
||||
'reply_variant' => $user->preference?->reply_variant,
|
||||
'reply_variant_default' => config('signature.default_reply_variant', 'short'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|null> $values clé de variable => valeur (null = rétablir l'annuaire)
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function update(DirectoryUser $user, array $values, ?string $replyVariant, bool $replyVariantProvided, string $actor): void
|
||||
{
|
||||
$editable = $this->editableFields($user)->keyBy('field');
|
||||
|
||||
$forbidden = array_diff(array_keys($values), $editable->keys()->all());
|
||||
if ($forbidden) {
|
||||
throw ValidationException::withMessages(
|
||||
collect($forbidden)->mapWithKeys(fn ($k) => ["fields.{$k}" => 'Ce champ n\'est pas modifiable.'])->all()
|
||||
);
|
||||
}
|
||||
|
||||
$rules = ['reply_variant' => ['nullable', Rule::in(['short', 'full', 'none'])]];
|
||||
foreach (array_keys($values) as $key) {
|
||||
$type = $editable[$key]->validation_rule ?: 'text';
|
||||
$rules["fields.{$key}"] = array_merge(['nullable'], self::TYPES[$type]['rule'] ?? self::TYPES['text']['rule']);
|
||||
}
|
||||
// Chaîne vide = masquer le champ : rien à valider pour elle.
|
||||
Validator::make(
|
||||
['fields' => array_filter($values, fn ($v) => $v !== ''), 'reply_variant' => $replyVariant],
|
||||
$rules,
|
||||
['fields.*.regex' => 'Numéro invalide (chiffres, espaces, + . - ( ) uniquement).', 'fields.*.max' => 'Valeur trop longue.'],
|
||||
)->validate();
|
||||
|
||||
DB::transaction(function () use ($user, $values, $replyVariant, $replyVariantProvided, $actor) {
|
||||
$changes = [];
|
||||
foreach ($values as $key => $value) {
|
||||
$value = $value === null ? null : trim((string) $value);
|
||||
$current = UserOverride::where('directory_user_id', $user->id)->where('field', $key)->first();
|
||||
|
||||
if ($value === null) {
|
||||
if ($current) {
|
||||
$changes[$key] = [$current->value, '(annuaire)'];
|
||||
$current->delete();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($current?->value === $value) {
|
||||
continue;
|
||||
}
|
||||
$changes[$key] = [$current?->value ?? '(annuaire)', $value === '' ? '(masqué)' : $value];
|
||||
UserOverride::updateOrCreate(['directory_user_id' => $user->id, 'field' => $key], ['value' => $value]);
|
||||
}
|
||||
|
||||
if ($replyVariantProvided) {
|
||||
$pref = UserPreference::firstOrNew(['directory_user_id' => $user->id]);
|
||||
if ($pref->reply_variant !== $replyVariant) {
|
||||
$changes['reply_variant'] = [$pref->reply_variant ?? '(défaut)', $replyVariant ?? '(défaut)'];
|
||||
$pref->reply_variant = $replyVariant;
|
||||
$pref->save();
|
||||
}
|
||||
}
|
||||
|
||||
if ($changes) {
|
||||
AuditLog::record('agent', $actor, 'profile.update', $user, $changes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function editableFields(DirectoryUser $user)
|
||||
{
|
||||
return EditableField::where('tenant_id', $user->tenant_id)->where('enabled', true)
|
||||
->whereIn('field', array_keys(VariableMap::FIELDS))
|
||||
->orderBy('id')->get();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user