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:
118
resources/js/components/declarations/BulkActionBar.vue
Normal file
118
resources/js/components/declarations/BulkActionBar.vue
Normal 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>
|
||||
Reference in New Issue
Block a user