Integration page webmail dans front end

This commit is contained in:
kevin 2026-05-04 11:32:50 +03:00
parent 9951ed0ee6
commit c4977bad15
11 changed files with 1901 additions and 1830 deletions

View File

@ -1,6 +1,97 @@
<template> <template>
<add-intervention-template> <client-detail-template>
<template #intervention-form> <template #button-return>
<div class="col-12">
<router-link
to="/interventions"
class="btn btn-outline-secondary btn-sm mb-3"
>
<i class="fas fa-arrow-left me-2"></i>Retour aux interventions
</router-link>
</div>
</template>
<template #client-detail-sidebar>
<div class="card position-sticky top-1 intervention-sidebar-card">
<div class="card-body text-center p-4">
<div class="avatar avatar-xxl position-relative mx-auto mb-3">
<div
class="intervention-avatar w-100 border-radius-xl shadow-sm d-flex align-items-center justify-content-center"
>
<i class="fas fa-notes-medical text-white text-xl"></i>
</div>
</div>
<soft-badge color="success" variant="gradient" size="sm" class="mb-3">
Nouvelle intervention
</soft-badge>
<h5 class="mb-1">Planifier une intervention</h5>
<p class="text-sm text-muted mb-0">
Préparez une fiche claire, liée au client et au défunt, dans le
style du dashboard.
</p>
</div>
</div>
<div class="card intervention-sidebar-card mt-4">
<div class="card-header pb-0">
<h6 class="mb-1">Repères</h6>
<p class="text-sm text-muted mb-0">
Les éléments ci-dessous suffisent pour démarrer correctement.
</p>
</div>
<div class="card-body pt-3">
<div class="check-item">
<span class="check-dot bg-gradient-success"></span>
<span class="text-sm">Associer le bon client</span>
</div>
<div class="check-item">
<span class="check-dot bg-gradient-info"></span>
<span class="text-sm">Définir le type d'intervention</span>
</div>
<div class="check-item">
<span class="check-dot bg-gradient-dark"></span>
<span class="text-sm">Renseigner la planification si connue</span>
</div>
</div>
</div>
</template>
<template #client-detail-content>
<div class="card intervention-hero-card mb-4">
<div class="card-body p-4">
<div
class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3"
>
<div>
<p
class="text-sm text-uppercase text-success font-weight-bold mb-2"
>
Dashboard intervention
</p>
<h4 class="mb-1">Créer une nouvelle intervention</h4>
<p class="text-sm text-muted mb-0">
Une mise en page plus propre, cohérente avec le reste de
l'application, centrée sur les informations métier utiles.
</p>
</div>
<div class="hero-pill-group">
<span class="hero-pill">
<i class="fas fa-user me-2 text-success"></i>Client
</span>
<span class="hero-pill">
<i class="fas fa-cross me-2 text-info"></i>Défunt
</span>
<span class="hero-pill">
<i class="fas fa-calendar-alt me-2 text-dark"></i>Planning
</span>
</div>
</div>
</div>
</div>
<intervention-form <intervention-form
:loading="loading" :loading="loading"
:validation-errors="validationErrors" :validation-errors="validationErrors"
@ -16,12 +107,14 @@
@create-intervention="handleCreateIntervention" @create-intervention="handleCreateIntervention"
/> />
</template> </template>
</add-intervention-template> </client-detail-template>
</template> </template>
<script setup> <script setup>
import AddInterventionTemplate from "@/components/templates/Interventions/AddInterventionTemplate.vue"; import ClientDetailTemplate from "@/components/templates/CRM/ClientDetailTemplate.vue";
import InterventionForm from "@/components/molecules/Interventions/InterventionForm.vue"; import InterventionForm from "@/components/molecules/Interventions/InterventionForm.vue";
import SoftBadge from "@/components/SoftBadge.vue";
import { defineProps, defineEmits } from "vue"; import { defineProps, defineEmits } from "vue";
import { RouterLink } from "vue-router";
defineProps({ defineProps({
loading: { loading: {
@ -76,3 +169,57 @@ const handleCreateIntervention = (data) => {
emit("createIntervention", data); emit("createIntervention", data);
}; };
</script> </script>
<style scoped>
.position-sticky {
top: 1rem;
}
.intervention-sidebar-card,
.intervention-hero-card {
border: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
}
.intervention-avatar {
width: 100px;
height: 100px;
background: linear-gradient(135deg, #17c1e8 0%, #3a416f 100%);
}
.check-item {
display: flex;
align-items: center;
gap: 0.75rem;
}
.check-item + .check-item {
margin-top: 0.9rem;
}
.check-dot {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
flex: 0 0 auto;
}
.hero-pill-group {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.hero-pill {
display: inline-flex;
align-items: center;
padding: 0.7rem 1rem;
border-radius: 999px;
background: rgba(248, 249, 250, 0.95);
color: #344767;
font-size: 0.875rem;
font-weight: 600;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
}
</style>

View File

@ -53,7 +53,9 @@ export default {
activeValue = active ? `active` : null; activeValue = active ? `active` : null;
return `${colorValue} ${sizeValue} ${fullWidthValue} ${activeValue}`; return [colorValue, sizeValue, fullWidthValue, activeValue]
.filter(Boolean)
.join(" ");
}, },
}, },
}; };

View File

@ -1,39 +0,0 @@
<template>
<div class="form-group">
<label :for="id" class="form-label">
<i class="fas fa-paperclip"></i> Pièces jointes
</label>
<input
:id="id"
type="file"
class="form-control"
multiple
@change="handleFileChange"
/>
<small class="text-muted"
>Formats acceptés: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG</small
>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({
id: {
type: String,
default: "webmailing-attachments",
},
});
const emit = defineEmits(["files-selected"]);
const handleFileChange = (event) => {
const files = event.target.files;
const fileList = Array.from(files).map((file) => ({
name: file.name,
size: file.size,
type: file.type,
}));
emit("files-selected", fileList);
};
</script>

View File

@ -1,64 +0,0 @@
<template>
<div class="form-group">
<textarea
:id="id"
v-model="localValue"
class="form-control"
:class="getClasses(error, success)"
placeholder="Contenu du message"
rows="8"
@blur="handleBlur"
></textarea>
</div>
</template>
<script setup>
import { ref, watch, defineEmits, defineProps } from "vue";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
id: {
type: String,
default: "webmailing-body",
},
error: {
type: Boolean,
default: false,
},
success: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "blur"]);
const localValue = ref(props.modelValue);
watch(
() => props.modelValue,
(newValue) => {
localValue.value = newValue;
}
);
watch(localValue, (newValue) => {
emit("update:modelValue", newValue);
});
const getClasses = (error, success) => {
if (error) {
return "is-invalid";
} else if (success) {
return "is-valid";
}
return "";
};
const handleBlur = () => {
emit("blur");
};
</script>

View File

@ -1,55 +0,0 @@
<template>
<soft-input
:id="id"
v-model="localValue"
type="text"
placeholder="Sujet du message"
:error="error"
:success="success"
@blur="handleBlur"
/>
</template>
<script setup>
import { defineEmits, defineProps } from "vue";
import { ref, watch } from "vue";
import SoftInput from "@/components/SoftInput.vue";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
id: {
type: String,
default: "webmailing-subject",
},
error: {
type: Boolean,
default: false,
},
success: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "blur"]);
const localValue = ref(props.modelValue);
watch(
() => props.modelValue,
(newValue) => {
localValue.value = newValue;
}
);
watch(localValue, (newValue) => {
emit("update:modelValue", newValue);
});
const handleBlur = () => {
emit("blur");
};
</script>

View File

@ -1,235 +1,328 @@
<template> <template>
<div class="card p-3 border-radius-xl bg-white" data-animation="FadeIn"> <form @submit.prevent="submitForm">
<h5 class="font-weight-bolder mb-0">Informations de l'intervention</h5> <div class="row g-4">
<p class="mb-0 text-sm">Créez une nouvelle intervention funéraire</p> <div class="col-12 col-xl-7">
<div class="card intervention-panel-card h-100" data-animation="FadeIn">
<div class="multisteps-form__content"> <div class="card-header pb-0 p-4">
<!-- Client selection --> <div
<div class="row mt-3"> class="d-flex align-items-center justify-content-between flex-wrap gap-2"
<div class="col-12"> >
<label class="form-label" <div>
>Client <span class="text-danger">*</span></label <h5 class="font-weight-bolder mb-1">
> Informations principales
<search-input </h5>
v-model="selectedItem" <p class="mb-0 text-sm text-muted">
:search-action="props.searchClients" Identifiez le client, le défunt concerné et le type
:min-chars="0" d'intervention.
item-key="id" </p>
item-label="name" </div>
@search="handleSearch" <soft-badge color="info" variant="gradient" size="sm">
@select="handleSelect" Champs essentiels
/> </soft-badge>
<div v-if="selectedItem" class="selected-item"> </div>
Sélectionné: {{ selectedItem.name }} ({{
selectedItem.email || "Pas d'email"
}})
</div> </div>
<div
v-if="fieldErrors.client_id" <div class="card-body pt-3 p-4">
class="invalid-feedback small-error" <div class="mb-4">
> <label class="form-label"
{{ fieldErrors.client_id }} >Client <span class="text-danger">*</span></label
>
<search-input
v-model="selectedItem"
:search-action="props.searchClients"
:min-chars="0"
item-key="id"
item-label="name"
@search="handleSearch"
@select="handleSelect"
/>
<div v-if="selectedItem" class="selection-chip mt-2">
<i class="fas fa-user me-2 text-success"></i>
<span>
{{ selectedItem.name }}
<small class="text-muted ms-1">{{
selectedItem.email || "Pas d'email"
}}</small>
</span>
</div>
<div
v-if="fieldErrors.client_id"
class="invalid-feedback small-error"
>
{{ fieldErrors.client_id }}
</div>
</div>
<div class="mb-4">
<label class="form-label">Personne décédée</label>
<search-input
v-model="selectedDeceased"
:search-action="props.searchDeceased"
:min-chars="0"
item-key="id"
:item-label="getDeceasedFullName"
@search="handleSearchDeceased"
@select="handleSelectDeceased"
/>
<div
v-if="selectedDeceased"
class="selection-chip mt-2 selection-chip-muted"
>
<i class="fas fa-cross me-2 text-info"></i>
<span>
{{ selectedDeceased.last_name }}
{{ selectedDeceased.first_name || "" }}
</span>
</div>
<div
v-if="fieldErrors.deceased_id"
class="invalid-feedback small-error"
>
{{ fieldErrors.deceased_id }}
</div>
</div>
<div class="row">
<div class="col-12 col-lg-6">
<label class="form-label"
>Type d'intervention <span class="text-danger">*</span></label
>
<select
v-model="form.type"
class="form-select soft-select"
:class="{ 'is-invalid': fieldErrors.type }"
>
<option value="">Sélectionnez un type d'intervention</option>
<option value="thanatopraxie">Thanatopraxie</option>
<option value="toilette_mortuaire">Toilette mortuaire</option>
<option value="exhumation">Exhumation</option>
<option value="retrait_pacemaker">Retrait pacemaker</option>
<option value="retrait_bijoux">Retrait bijoux</option>
<option value="autre">Autre</option>
</select>
<div
v-if="fieldErrors.type"
class="invalid-feedback small-error"
>
{{ fieldErrors.type }}
</div>
</div>
<div class="col-12 col-lg-6 mt-4 mt-lg-0">
<label class="form-label">Donneur d'ordre</label>
<soft-input
v-model="form.order_giver"
:class="{ 'is-invalid': fieldErrors.order_giver }"
type="text"
placeholder="Nom du donneur d'ordre"
/>
<div
v-if="fieldErrors.order_giver"
class="invalid-feedback small-error"
>
{{ fieldErrors.order_giver }}
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Deceased selection --> <div class="col-12 col-xl-5">
<div class="row mt-3"> <div class="card intervention-panel-card h-100">
<div class="col-12"> <div class="card-header pb-0 p-4">
<label class="form-label">Personne décédée</label> <div
<search-input class="d-flex align-items-center justify-content-between flex-wrap gap-2"
v-model="selectedDeceased" >
:search-action="props.searchDeceased" <div>
:min-chars="0" <h5 class="font-weight-bolder mb-1">Planification</h5>
item-key="id" <p class="mb-0 text-sm text-muted">
:item-label="getDeceasedFullName" Définissez la date, l'heure, la durée et l'état de suivi.
@search="handleSearchDeceased" </p>
@select="handleSelectDeceased" </div>
/> <soft-badge color="success" variant="gradient" size="sm">
<div v-if="selectedDeceased" class="selected-item"> Organisation
Sélectionné: {{ selectedDeceased.last_name }} </soft-badge>
{{ selectedDeceased.first_name || "" }} </div>
</div> </div>
<div
v-if="fieldErrors.deceased_id" <div class="card-body pt-3 p-4">
class="invalid-feedback small-error" <div class="row g-3">
> <div class="col-12 col-sm-6">
{{ fieldErrors.deceased_id }} <label class="form-label">Date de l'intervention</label>
<soft-input
v-model="form.scheduled_date"
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
type="date"
/>
</div>
<div class="col-12 col-sm-6">
<label class="form-label">Heure de l'intervention</label>
<soft-input
v-model="form.scheduled_time"
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
type="time"
/>
</div>
<div
v-if="fieldErrors.scheduled_at"
class="col-12 invalid-feedback small-error"
>
{{ fieldErrors.scheduled_at }}
</div>
<div class="col-12 col-sm-6">
<label class="form-label">Durée (minutes)</label>
<soft-input
v-model="form.duration_min"
:class="{ 'is-invalid': fieldErrors.duration_min }"
type="number"
min="1"
placeholder="ex. 90"
/>
<div
v-if="fieldErrors.duration_min"
class="invalid-feedback small-error"
>
{{ fieldErrors.duration_min }}
</div>
</div>
<div class="col-12 col-sm-6">
<label class="form-label">Statut</label>
<select
v-model="form.status"
class="form-select soft-select"
:class="{ 'is-invalid': fieldErrors.status }"
>
<option value="">Sélectionnez un statut</option>
<option value="demande">Demande</option>
<option value="planifie">Planifié</option>
<option value="en_cours">En cours</option>
<option value="termine">Terminé</option>
<option value="annule">Annulé</option>
</select>
<div
v-if="fieldErrors.status"
class="invalid-feedback small-error"
>
{{ fieldErrors.status }}
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Intervention type --> <div class="col-12">
<div class="row mt-3"> <div class="card intervention-panel-card">
<div class="col-12"> <div class="card-header pb-0 p-4">
<label class="form-label" <div
>Type d'intervention <span class="text-danger">*</span></label class="d-flex align-items-center justify-content-between flex-wrap gap-2"
> >
<select <div>
v-model="form.type" <h5 class="font-weight-bolder mb-1">Observations</h5>
class="form-select multisteps-form__select" <p class="mb-0 text-sm text-muted">
:class="{ 'is-invalid': fieldErrors.type }" Ajoutez le contexte utile pour l'équipe et la coordination
> terrain.
<option value="">Sélectionnez un type d'intervention</option> </p>
<option value="thanatopraxie">Thanatopraxie</option> </div>
<option value="toilette_mortuaire">Toilette mortuaire</option> <soft-badge color="dark" variant="gradient" size="sm">
<option value="exhumation">Exhumation</option> Notes métier
<option value="retrait_pacemaker">Retrait pacemaker</option> </soft-badge>
<option value="retrait_bijoux">Retrait bijoux</option> </div>
<option value="autre">Autre</option>
</select>
<div v-if="fieldErrors.type" class="invalid-feedback small-error">
{{ fieldErrors.type }}
</div> </div>
</div>
</div>
<!-- Date et heure de l'intervention --> <div class="card-body pt-3 p-4">
<div class="row mt-3"> <div class="row g-4 align-items-start">
<div class="col-12 col-sm-6"> <div class="col-12 col-xl-8">
<label class="form-label">Date de l'intervention</label> <label class="form-label">Notes et observations</label>
<soft-input <textarea
v-model="form.scheduled_date" v-model="form.notes"
class="multisteps-form__input" class="form-control soft-textarea"
:class="{ 'is-invalid': fieldErrors.scheduled_at }" :class="{ 'is-invalid': fieldErrors.notes }"
type="date" rows="5"
/> placeholder="Informations complémentaires, instructions spéciales..."
<div maxlength="2000"
v-if="fieldErrors.scheduled_at" ></textarea>
class="invalid-feedback small-error" <div
> v-if="fieldErrors.notes"
{{ fieldErrors.scheduled_at }} class="invalid-feedback small-error"
</div> >
</div> {{ fieldErrors.notes }}
<div class="col-12 col-sm-6 mt-3 mt-sm-0"> </div>
<label class="form-label">Heure de l'intervention</label> </div>
<soft-input
v-model="form.scheduled_time"
class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.scheduled_at }"
type="time"
/>
<div
v-if="fieldErrors.scheduled_at"
class="invalid-feedback small-error"
>
{{ fieldErrors.scheduled_at }}
</div>
</div>
</div>
<!-- Duration and status --> <div class="col-12 col-xl-4">
<div class="row mt-3"> <div class="summary-panel">
<div class="col-12 col-sm-6"> <p class="summary-title mb-3">Résumé rapide</p>
<label class="form-label">Durée (minutes)</label> <div class="summary-line">
<soft-input <span>Client</span>
v-model="form.duration_min" <strong>{{
class="multisteps-form__input" selectedItem?.name || "Non sélectionné"
:class="{ 'is-invalid': fieldErrors.duration_min }" }}</strong>
type="number" </div>
min="1" <div class="summary-line">
placeholder="ex. 90" <span>Défunt</span>
/> <strong>{{
<div selectedDeceased
v-if="fieldErrors.duration_min" ? getDeceasedFullName(selectedDeceased)
class="invalid-feedback small-error" : "Non sélectionné"
> }}</strong>
{{ fieldErrors.duration_min }} </div>
</div> <div class="summary-line">
</div> <span>Type</span>
<div class="col-12 col-sm-6 mt-3 mt-sm-0"> <strong>{{ form.type || "Non défini" }}</strong>
<label class="form-label">Statut</label> </div>
<select <div class="summary-line">
v-model="form.status" <span>Statut</span>
class="form-select multisteps-form__select" <strong>{{ form.status || "Non défini" }}</strong>
:class="{ 'is-invalid': fieldErrors.status }" </div>
> </div>
<option value="">Sélectionnez un statut</option> </div>
<option value="demande">Demande</option> </div>
<option value="planifie">Planifié</option>
<option value="en_cours">En cours</option>
<option value="termine">Terminé</option>
<option value="annule">Annulé</option>
</select>
<div v-if="fieldErrors.status" class="invalid-feedback small-error">
{{ fieldErrors.status }}
</div>
</div>
</div>
<!-- Order giver and notes --> <div
<div class="row mt-3"> class="button-row d-flex justify-content-end flex-wrap gap-2 mt-4"
<div class="col-12"> >
<label class="form-label">Donneur d'ordre</label> <soft-button
<soft-input type="button"
v-model="form.order_giver" color="secondary"
class="multisteps-form__input" variant="outline"
:class="{ 'is-invalid': fieldErrors.order_giver }" class="mb-0"
type="text" @click="resetForm"
placeholder="Nom du donneur d'ordre" >
/> Réinitialiser
<div </soft-button>
v-if="fieldErrors.order_giver" <soft-button
class="invalid-feedback small-error" type="submit"
> color="success"
{{ fieldErrors.order_giver }} variant="gradient"
class="mb-0"
:disabled="props.loading"
>
<span
v-if="props.loading"
class="spinner-border spinner-border-sm me-2"
role="status"
></span>
{{
props.loading ? "Enregistrement..." : "Créer l'intervention"
}}
</soft-button>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Notes -->
<div class="row mt-3">
<div class="col-12">
<label class="form-label">Notes et observations</label>
<textarea
v-model="form.notes"
class="form-control multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.notes }"
rows="4"
placeholder="Informations complémentaires, instructions spéciales..."
maxlength="2000"
></textarea>
<div v-if="fieldErrors.notes" class="invalid-feedback small-error">
{{ fieldErrors.notes }}
</div>
</div>
</div>
<!-- Boutons -->
<div class="button-row d-flex mt-4">
<soft-button
type="button"
color="secondary"
variant="outline"
class="me-2 mb-0"
@click="resetForm"
>
Réinitialiser
</soft-button>
<soft-button
type="button"
color="dark"
variant="gradient"
class="ms-auto mb-0"
:disabled="props.loading"
@click="submitForm"
>
<span
v-if="props.loading"
class="spinner-border spinner-border-sm me-2"
role="status"
></span>
{{ props.loading ? "Enregistrement..." : "Enregistrer" }}
</soft-button>
</div>
</div> </div>
</div> </form>
</template> </template>
<script setup> <script setup>
import { ref, defineProps, defineEmits, watch } from "vue"; import { ref, defineProps, defineEmits, watch } from "vue";
import SoftInput from "@/components/SoftInput.vue"; import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue"; import SoftButton from "@/components/SoftButton.vue";
import SoftBadge from "@/components/SoftBadge.vue";
import SearchInput from "@/components/atoms/input/SearchInput.vue"; import SearchInput from "@/components/atoms/input/SearchInput.vue";
// Props // Props
@ -484,9 +577,15 @@ const clearErrors = () => {
</script> </script>
<style scoped> <style scoped>
.intervention-panel-card {
border: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
}
.form-label { .form-label {
font-weight: 600; font-weight: 600;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
color: #344767;
} }
.text-danger { .text-danger {
@ -502,6 +601,21 @@ const clearErrors = () => {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.selection-chip {
display: inline-flex;
align-items: center;
padding: 0.55rem 0.85rem;
border-radius: 0.85rem;
background: rgba(45, 206, 137, 0.08);
color: #344767;
font-size: 0.85rem;
font-weight: 600;
}
.selection-chip-muted {
background: rgba(23, 193, 232, 0.08);
}
.spinner-border-sm { .spinner-border-sm {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
@ -521,19 +635,57 @@ const clearErrors = () => {
font-size: 0.8rem; font-size: 0.8rem;
} }
.multisteps-form__select { .soft-select,
.soft-textarea {
background-color: white; background-color: white;
border: 1px solid #d2d6da; border: 1px solid #d2d6da;
border-radius: 0.5rem; border-radius: 0.75rem;
color: #495057; color: #495057;
padding: 0.5rem 0.75rem; padding: 0.7rem 0.9rem;
font-size: 0.875rem; font-size: 0.875rem;
transition: all 0.15s ease-in-out; transition: all 0.15s ease-in-out;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
} }
.multisteps-form__select:focus { .soft-select:focus,
.soft-textarea:focus {
border-color: #5e72e4; border-color: #5e72e4;
box-shadow: 0 0 0 0.2rem rgba(94, 114, 228, 0.25); box-shadow: 0 0 0 0.2rem rgba(94, 114, 228, 0.25);
outline: 0; outline: 0;
} }
.summary-panel {
border-radius: 1rem;
padding: 1rem;
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
border: 1px solid rgba(52, 71, 103, 0.08);
}
.summary-title {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #8392ab;
font-weight: 700;
}
.summary-line {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
font-size: 0.875rem;
color: #67748e;
}
.summary-line + .summary-line {
margin-top: 0.85rem;
padding-top: 0.85rem;
border-top: 1px solid rgba(52, 71, 103, 0.08);
}
.summary-line strong {
color: #344767;
text-align: right;
}
</style> </style>

View File

@ -1,150 +0,0 @@
<template>
<div class="webmailing-form">
<div class="form-section mb-4">
<h5 class="mb-3">Destinataires</h5>
<soft-input
v-model="formData.recipients"
type="email"
placeholder="Entrez les adresses email (séparées par des virgules)"
icon="fas fa-envelope"
icon-dir="left"
/>
</div>
<div class="form-section mb-4">
<h5 class="mb-3">Sujet</h5>
<webmailing-subject-input
v-model="formData.subject"
@blur="validateSubject"
/>
</div>
<div class="form-section mb-4">
<h5 class="mb-3">Contenu du message</h5>
<webmailing-body-input v-model="formData.body" @blur="validateBody" />
</div>
<div class="form-section mb-4">
<h5 class="mb-3">Pièces jointes</h5>
<webmailing-attachment @files-selected="handleFilesSelected" />
<div v-if="formData.attachments.length > 0" class="mt-3">
<h6>Fichiers sélectionnés:</h6>
<ul class="list-unstyled">
<li
v-for="file in formData.attachments"
:key="file.name"
class="mb-2"
>
<span class="badge bg-info">{{ file.name }}</span>
<small class="ms-2 text-muted"
>({{ formatFileSize(file.size) }})</small
>
</li>
</ul>
</div>
</div>
<div class="form-section">
<h5 class="mb-3">Options</h5>
<div class="form-check mb-2">
<input
id="send-copy"
v-model="formData.sendCopy"
type="checkbox"
class="form-check-input"
/>
<label class="form-check-label" for="send-copy">
M'envoyer une copie
</label>
</div>
<div class="form-check">
<input
id="send-scheduled"
v-model="formData.scheduled"
type="checkbox"
class="form-check-input"
/>
<label class="form-check-label" for="send-scheduled">
Programmer l'envoi
</label>
</div>
<div v-if="formData.scheduled" class="mt-3">
<soft-input
v-model="formData.scheduledDate"
type="datetime-local"
placeholder="Date et heure d'envoi"
icon="fas fa-calendar"
icon-dir="left"
/>
</div>
</div>
</div>
</template>
<script setup>
import { defineEmits, defineProps } from "vue";
import { ref } from "vue";
import SoftInput from "@/components/SoftInput.vue";
import WebmailingSubjectInput from "@/components/atoms/Webmailing/WebmailingSubjectInput.vue";
import WebmailingBodyInput from "@/components/atoms/Webmailing/WebmailingBodyInput.vue";
import WebmailingAttachment from "@/components/atoms/Webmailing/WebmailingAttachment.vue";
const props = defineProps({
initialData: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["form-data-change"]);
const formData = ref({
recipients: props.initialData.recipients || "",
subject: props.initialData.subject || "",
body: props.initialData.body || "",
attachments: props.initialData.attachments || [],
sendCopy: props.initialData.sendCopy || false,
scheduled: props.initialData.scheduled || false,
scheduledDate: props.initialData.scheduledDate || "",
});
const validateSubject = () => {
// Add validation logic if needed
};
const validateBody = () => {
// Add validation logic if needed
};
const handleFilesSelected = (files) => {
formData.value.attachments = files;
emitFormData();
};
const emitFormData = () => {
emit("form-data-change", formData.value);
};
const formatFileSize = (bytes) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
};
</script>
<style scoped>
.webmailing-form {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
}
.form-section {
background-color: white;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #17a2b8;
}
</style>

View File

@ -1,109 +0,0 @@
<template>
<div class="webmailing-list">
<div v-if="emails.length === 0" class="alert alert-info">
<i class="fas fa-info-circle"></i> Aucun email envoyé
</div>
<div v-else class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Destinataires</th>
<th>Sujet</th>
<th>Date d'envoi</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="email in emails" :key="email.id">
<td>
<small>{{ email.recipients }}</small>
</td>
<td>
<strong>{{ email.subject }}</strong>
</td>
<td>
<small>{{ formatDate(email.sentDate) }}</small>
</td>
<td>
<span :class="getStatusClass(email.status)">
{{ getStatusLabel(email.status) }}
</span>
</td>
<td>
<button class="btn btn-sm btn-info" @click="viewEmail(email.id)">
<i class="fas fa-eye"></i>
</button>
<button
class="btn btn-sm btn-danger ms-2"
@click="deleteEmail(email.id)"
>
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({
emails: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["view-email", "delete-email"]);
const formatDate = (date) => {
if (!date) return "-";
return new Date(date).toLocaleDateString("fr-FR", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const getStatusClass = (status) => {
const statusClasses = {
sent: "badge bg-success",
pending: "badge bg-warning",
failed: "badge bg-danger",
scheduled: "badge bg-info",
};
return statusClasses[status] || "badge bg-secondary";
};
const getStatusLabel = (status) => {
const statusLabels = {
sent: "Envoyé",
pending: "En attente",
failed: "Échoué",
scheduled: "Programmé",
};
return statusLabels[status] || "Inconnu";
};
const viewEmail = (id) => {
emit("view-email", id);
};
const deleteEmail = (id) => {
emit("delete-email", id);
};
</script>
<style scoped>
.webmailing-list {
background-color: white;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@ -1,25 +1,40 @@
<template> <template>
<div class="container-fluid py-4"> <section class="container-fluid py-4">
<slot name="webmailing-header"></slot> <div class="card webmail-layout-card">
<div class="webmail-shell__frame">
<div class="card shadow-lg"> <slot name="webmailing-sidebar"></slot>
<div class="card-body"> <slot name="webmailing-list"></slot>
<slot name="webmailing-tabs"></slot> <slot name="webmailing-detail"></slot>
<slot name="webmailing-content"></slot>
</div> </div>
</div> </div>
</div> </section>
</template> </template>
<script></script>
<style scoped> <style scoped>
.card { .webmail-layout-card {
border: none; border: 0;
border-radius: 12px; border-radius: 1rem;
box-shadow: 0 20px 30px rgba(15, 23, 42, 0.08);
} }
.card-body { .webmail-shell__frame {
padding: 2rem; display: grid;
grid-template-columns: 220px 320px minmax(0, 1fr);
min-height: calc(100vh - 185px);
background: #ffffff;
border-radius: 1rem;
overflow: hidden;
}
@media (max-width: 1200px) {
.webmail-shell__frame {
grid-template-columns: 210px 300px minmax(0, 1fr);
}
}
@media (max-width: 992px) {
.webmail-shell__frame {
grid-template-columns: 1fr;
}
} }
</style> </style>