feat: Provisionner automatiquement un nouvel établissement
Lorsqu'un super-admin crée un établissement via l'interface, le système doit automatiquement créer la base tenant, exécuter les migrations, créer le premier utilisateur admin et envoyer l'invitation — le tout de manière asynchrone pour ne pas bloquer la réponse HTTP. Ce mécanisme rend chaque établissement opérationnel dès sa création sans intervention manuelle sur l'infrastructure.
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
let files = $state<UploadedFile[]>(existingFiles);
|
||||
let pendingFiles = $state<{ name: string; size: number }[]>([]);
|
||||
let error = $state<string | null>(null);
|
||||
let isDragging = $state(false);
|
||||
let fileInput: HTMLInputElement;
|
||||
|
||||
$effect(() => {
|
||||
@@ -52,6 +53,15 @@
|
||||
return '📎';
|
||||
}
|
||||
|
||||
function formatFileType(mimeType: string): string {
|
||||
if (mimeType === 'application/pdf') return 'PDF';
|
||||
if (mimeType === 'image/jpeg') return 'JPEG';
|
||||
if (mimeType === 'image/png') return 'PNG';
|
||||
if (mimeType.includes('wordprocessingml')) return 'DOCX';
|
||||
const parts = mimeType.split('/');
|
||||
return parts[1]?.toUpperCase() ?? mimeType;
|
||||
}
|
||||
|
||||
function validateFile(file: File): string | null {
|
||||
if (!acceptedTypes.includes(file.type)) {
|
||||
return `Type de fichier non accepté : ${file.type}.`;
|
||||
@@ -62,14 +72,10 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleFileSelect(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const selectedFiles = input.files;
|
||||
if (!selectedFiles || selectedFiles.length === 0) return;
|
||||
|
||||
async function processFiles(fileList: globalThis.FileList) {
|
||||
error = null;
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
for (const file of fileList) {
|
||||
const validationError = validateFile(file);
|
||||
if (validationError) {
|
||||
error = validationError;
|
||||
@@ -87,10 +93,37 @@
|
||||
pendingFiles = pendingFiles.filter((p) => p.name !== file.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileSelect(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const selectedFiles = input.files;
|
||||
if (!selectedFiles || selectedFiles.length === 0) return;
|
||||
|
||||
await processFiles(selectedFiles);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
isDragging = true;
|
||||
}
|
||||
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
const related = event.relatedTarget as globalThis.Node | null;
|
||||
if (related && target.contains(related)) return;
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
async function handleDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
isDragging = false;
|
||||
const droppedFiles = event.dataTransfer?.files;
|
||||
if (!droppedFiles || droppedFiles.length === 0) return;
|
||||
await processFiles(droppedFiles);
|
||||
}
|
||||
|
||||
async function handleDelete(fileId: string) {
|
||||
error = null;
|
||||
|
||||
@@ -114,6 +147,7 @@
|
||||
<li class="file-item">
|
||||
<span class="file-icon">{getFileIcon(file.mimeType)}</span>
|
||||
<span class="file-name">{file.filename}</span>
|
||||
<span class="file-type">{formatFileType(file.mimeType)}</span>
|
||||
<span class="file-size">{formatFileSize(file.fileSize)}</span>
|
||||
{#if !disabled && showDelete}
|
||||
<button
|
||||
@@ -140,22 +174,38 @@
|
||||
{/if}
|
||||
|
||||
{#if !disabled}
|
||||
<button type="button" class="upload-btn" onclick={() => fileInput.click()}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48" />
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="drop-zone"
|
||||
class:drop-zone-active={isDragging}
|
||||
ondragover={handleDragOver}
|
||||
ondragleave={handleDragLeave}
|
||||
ondrop={handleDrop}
|
||||
onclick={() => fileInput.click()}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
|
||||
<polyline points="17 8 12 3 7 8" />
|
||||
<line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
Ajouter un fichier
|
||||
</button>
|
||||
<p class="drop-zone-text">
|
||||
Glissez-déposez vos fichiers ici ou
|
||||
<button type="button" class="drop-zone-browse" onclick={(e) => { e.stopPropagation(); fileInput.click(); }}>
|
||||
parcourir
|
||||
</button>
|
||||
</p>
|
||||
<p class="upload-hint">{hint}</p>
|
||||
</div>
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
type="file"
|
||||
accept={acceptAttr}
|
||||
onchange={handleFileSelect}
|
||||
multiple
|
||||
class="file-input-hidden"
|
||||
aria-hidden="true"
|
||||
tabindex="-1"
|
||||
/>
|
||||
<p class="upload-hint">{hint}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -216,6 +266,16 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-type {
|
||||
color: #6b7280;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
background: #f3f4f6;
|
||||
border-radius: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: #9ca3af;
|
||||
font-size: 0.75rem;
|
||||
@@ -249,25 +309,51 @@
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: inline-flex;
|
||||
.drop-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px dashed #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
background: white;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
gap: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
background: #fafafa;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background-color 0.15s;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
border-color: #3b82f6;
|
||||
.drop-zone:hover {
|
||||
border-color: #93c5fd;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.drop-zone-active {
|
||||
border-color: #3b82f6;
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.drop-zone-text {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.drop-zone-browse {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #2563eb;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.drop-zone-browse:hover {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.file-input-hidden {
|
||||
|
||||
Reference in New Issue
Block a user