feat: add bulk client notifications and email enhancements with review fixes (Stories 3.4 & 3.5)

Story 3-4: Bulk client notification scheduling — BulkNotificationController,
BulkActionBar component, checkbox selection on declarations index.

Story 3-5: Email notification enhancement — observer-driven email on
en_attente_client, cache invalidation on ferme, workspace branding on
all email templates, 11 feature tests.

Code review fixes:
- Move bulk-notify route above resource wildcard to prevent shadowing
- Add static $suppressEmail flag to prevent observer double-sending
  when DeclarationMessageController already sends the email
- Fix canBulkNotify logic (was granting workers access)
- Add WorkspaceUserRole check to BulkNotifyRequest::authorize()
- Replace firstOrCreate with explicit invitation lookup that syncs
  client email and handles used/expired invitations correctly
- Watch declarations.data instead of current_page to clear selection
  on filter/sort changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 14:31:36 +01:00
parent 32e11db2b5
commit 1d4f3bcd0f
17 changed files with 1384 additions and 7 deletions

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
import { ref } from 'vue';
import { router } from '@inertiajs/vue3';
import { Loader2, Mail, X } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
type Props = {
selectedIds: number[];
bulkNotifyUrl: string;
};
type Emits = {
clear: [];
};
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const showConfirmDialog = ref(false);
const processing = ref(false);
function sendNotifications() {
processing.value = true;
router.post(
props.bulkNotifyUrl,
{ declaration_ids: props.selectedIds },
{
onSuccess: () => {
showConfirmDialog.value = false;
processing.value = false;
emit('clear');
},
onError: () => {
processing.value = false;
},
},
);
}
</script>
<template>
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-full opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y-full opacity-0"
>
<div
v-if="selectedIds.length > 0"
class="fixed inset-x-0 bottom-0 z-50 border-t bg-background p-4 shadow-lg md:p-3"
>
<div
class="mx-auto flex max-w-7xl flex-col items-center gap-3 md:flex-row md:justify-between"
>
<div class="flex items-center gap-3">
<span class="text-sm font-medium">
{{ selectedIds.length }} selectionne{{
selectedIds.length > 1 ? 's' : ''
}}
</span>
<Button
variant="ghost"
size="sm"
@click="emit('clear')"
>
<X class="mr-1 h-4 w-4" />
Effacer
</Button>
</div>
<Button @click="showConfirmDialog = true">
<Mail class="mr-2 h-4 w-4" />
Notifier les clients
</Button>
</div>
</div>
</Transition>
<Dialog v-model:open="showConfirmDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>Confirmer l'envoi</DialogTitle>
<DialogDescription>
Vous allez envoyer une notification par email a
{{ selectedIds.length }} client{{
selectedIds.length > 1 ? 's' : ''
}}
pour demander les documents manquants.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
:disabled="processing"
@click="showConfirmDialog = false"
>
Annuler
</Button>
<Button :disabled="processing" @click="sendNotifications">
<Loader2
v-if="processing"
class="mr-2 h-4 w-4 animate-spin"
/>
Envoyer les notifications
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>