364 lines
10 KiB
Vue
364 lines
10 KiB
Vue
<script setup>
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
import { useRoute } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useClientsStore } from '@/features/clients/stores/clientsStore.js';
|
|
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
|
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
|
|
|
|
const route = useRoute();
|
|
const workspaceStore = useWorkspaceStore();
|
|
const clientsStore = useClientsStore();
|
|
const campaignsStore = useCampaignsStore();
|
|
const { t } = useI18n();
|
|
const isCreateFormVisible = ref(false);
|
|
const formError = ref(null);
|
|
|
|
const form = reactive({
|
|
name: '',
|
|
startDate: '',
|
|
endDate: '',
|
|
description: '',
|
|
notes: '',
|
|
});
|
|
|
|
const operationalClient = computed(() => clientsStore.operationalClient);
|
|
|
|
function resetForm() {
|
|
form.name = '';
|
|
form.startDate = '';
|
|
form.endDate = '';
|
|
form.description = '';
|
|
form.notes = '';
|
|
formError.value = null;
|
|
}
|
|
|
|
function openCreateForm() {
|
|
resetForm();
|
|
isCreateFormVisible.value = true;
|
|
}
|
|
|
|
async function submitForm() {
|
|
if (campaignsStore.isCreating) {
|
|
return;
|
|
}
|
|
|
|
formError.value = null;
|
|
|
|
if (!form.name || !form.startDate || !form.endDate) {
|
|
formError.value = t('campaigns.errors.required');
|
|
return;
|
|
}
|
|
|
|
if (new Date(form.endDate) < new Date(form.startDate)) {
|
|
formError.value = t('campaigns.errors.invalidDateRange');
|
|
return;
|
|
}
|
|
|
|
if (!operationalClient.value?.id) {
|
|
formError.value = t('campaigns.errors.workspaceAccountRequired');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await campaignsStore.createCampaign({
|
|
clientId: operationalClient.value.id,
|
|
name: form.name,
|
|
startDate: new Date(form.startDate).toISOString(),
|
|
endDate: new Date(form.endDate).toISOString(),
|
|
description: form.description,
|
|
notes: form.notes,
|
|
});
|
|
|
|
isCreateFormVisible.value = false;
|
|
resetForm();
|
|
} catch (error) {
|
|
formError.value = t('campaigns.errors.createFailed');
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => route.query.create,
|
|
createValue => {
|
|
if (createValue === 'true') {
|
|
openCreateForm();
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
function formatCampaignDateRange(campaign) {
|
|
if (!campaign?.startDate || !campaign?.endDate) {
|
|
return t('campaigns.noDateRange');
|
|
}
|
|
|
|
const start = new Date(campaign.startDate);
|
|
const end = new Date(campaign.endDate);
|
|
return new Intl.DateTimeFormat(undefined, {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
}).formatRange(start, end);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<section class="page-shell">
|
|
<div class="header">
|
|
<div>
|
|
<div class="eyebrow">{{ t('campaigns.eyebrow') }}</div>
|
|
<h1>{{ t('campaigns.title') }}</h1>
|
|
<p>{{ t('campaigns.description') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="isCreateFormVisible"
|
|
class="create-panel"
|
|
>
|
|
<div class="panel-header">
|
|
<strong>{{ t('campaigns.createTitle') }}</strong>
|
|
<span>{{ workspaceStore.activeWorkspace?.name }}</span>
|
|
</div>
|
|
|
|
<div
|
|
v-if="formError"
|
|
class="page-message error"
|
|
>
|
|
{{ formError }}
|
|
</div>
|
|
|
|
<div class="form-grid">
|
|
<v-text-field
|
|
v-model="form.startDate"
|
|
:label="t('campaigns.fields.startDate')"
|
|
:disabled="campaignsStore.isCreating"
|
|
type="date"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="form.endDate"
|
|
:label="t('campaigns.fields.endDate')"
|
|
:disabled="campaignsStore.isCreating"
|
|
type="date"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="form.name"
|
|
class="field-wide"
|
|
:label="t('campaigns.fields.name')"
|
|
:disabled="campaignsStore.isCreating"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
|
|
<v-textarea
|
|
v-model="form.description"
|
|
class="field-wide"
|
|
:label="t('campaigns.fields.description')"
|
|
:disabled="campaignsStore.isCreating"
|
|
rows="3"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
|
|
<v-textarea
|
|
v-model="form.notes"
|
|
class="field-wide"
|
|
:label="t('campaigns.fields.notes')"
|
|
:disabled="campaignsStore.isCreating"
|
|
rows="3"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
</div>
|
|
|
|
<div class="panel-actions">
|
|
<v-btn variant="text" :ripple="false"
|
|
class="secondary"
|
|
:disabled="campaignsStore.isCreating"
|
|
@click="isCreateFormVisible = false"
|
|
>
|
|
{{ t('common.cancel') }}
|
|
</v-btn>
|
|
<v-btn variant="text" :ripple="false"
|
|
class="primary"
|
|
:disabled="campaignsStore.isCreating"
|
|
@click="submitForm"
|
|
>
|
|
<v-progress-circular
|
|
v-if="campaignsStore.isCreating"
|
|
indeterminate
|
|
:size="16"
|
|
:width="2"
|
|
/>
|
|
<span>{{ campaignsStore.isCreating ? t('common.creating') : t('campaigns.createTitle') }}</span>
|
|
</v-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="campaignsStore.isLoading"
|
|
class="page-message"
|
|
>
|
|
{{ t('campaigns.loading') }}
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="campaignsStore.error"
|
|
class="page-message error"
|
|
>
|
|
{{ campaignsStore.error }}
|
|
</div>
|
|
|
|
<div class="campaign-stack">
|
|
<router-link
|
|
v-for="campaign in campaignsStore.campaigns"
|
|
:key="campaign.id"
|
|
:to="{ name: 'campaign-detail', params: { campaignId: campaign.id } }"
|
|
class="campaign-row"
|
|
>
|
|
<div>
|
|
<strong>{{ campaign.name }}</strong>
|
|
<span>{{ campaign.description || campaign.status }}</span>
|
|
</div>
|
|
<div class="campaign-meta">
|
|
<span>{{ workspaceStore.activeWorkspace?.name || t('nav.noWorkspace') }}</span>
|
|
<em>{{ formatCampaignDateRange(campaign) }}</em>
|
|
</div>
|
|
</router-link>
|
|
</div>
|
|
|
|
<div
|
|
v-if="!campaignsStore.isLoading && !campaignsStore.campaigns.length"
|
|
class="page-message"
|
|
>
|
|
{{ t('campaigns.empty') }}
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@reference "@/assets/main.css";
|
|
.page-shell {
|
|
@apply mx-auto flex w-full max-w-7xl flex-col gap-6 px-5 py-8 md:px-8;
|
|
}
|
|
|
|
.eyebrow {
|
|
@apply text-xs font-bold uppercase tracking-[0.24em];
|
|
color: var(--app-color-on-tertiary);
|
|
}
|
|
|
|
.header h1 {
|
|
@apply mt-2 text-4xl font-black;
|
|
color: var(--app-color-on-surface);
|
|
}
|
|
|
|
.header p,
|
|
.panel-header span,
|
|
.campaign-row span,
|
|
.campaign-meta span,
|
|
.campaign-meta em {
|
|
@apply text-sm leading-6 not-italic;
|
|
color: var(--app-text-muted);
|
|
}
|
|
|
|
.primary,
|
|
.secondary {
|
|
@apply inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-bold transition;
|
|
}
|
|
|
|
.primary {
|
|
background: var(--app-color-on-surface);
|
|
color: var(--app-color-on-primary);
|
|
}
|
|
|
|
.secondary {
|
|
background: var(--app-control-hover);
|
|
color: var(--app-color-on-surface);
|
|
}
|
|
|
|
.create-panel,
|
|
.campaign-row {
|
|
@apply rounded-[1.5rem] border;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border-color: var(--app-border-subtle);
|
|
}
|
|
|
|
.create-panel {
|
|
@apply flex flex-col gap-5 p-5;
|
|
}
|
|
|
|
.panel-header {
|
|
@apply flex flex-col gap-2;
|
|
}
|
|
|
|
.panel-header strong,
|
|
.campaign-row strong {
|
|
color: var(--app-color-on-surface);
|
|
}
|
|
|
|
.form-grid {
|
|
@apply grid gap-4 md:grid-cols-2;
|
|
}
|
|
|
|
.field {
|
|
@apply flex flex-col gap-2 text-sm font-semibold;
|
|
color: var(--app-color-on-surface);
|
|
}
|
|
|
|
.field-wide {
|
|
@apply md:col-span-2;
|
|
}
|
|
|
|
.field input {
|
|
@apply rounded-2xl border px-4 py-3 text-sm;
|
|
border-color: var(--app-border-subtle);
|
|
background: rgba(255, 255, 255, 0.95);
|
|
color: var(--app-color-on-surface);
|
|
}
|
|
|
|
.field textarea {
|
|
@apply min-h-28 rounded-2xl border px-4 py-3 text-sm;
|
|
border-color: var(--app-border-subtle);
|
|
background: rgba(255, 255, 255, 0.95);
|
|
color: var(--app-color-on-surface);
|
|
resize: vertical;
|
|
}
|
|
|
|
.panel-actions {
|
|
@apply flex justify-end gap-3;
|
|
}
|
|
|
|
.campaign-stack {
|
|
@apply flex flex-col gap-4;
|
|
}
|
|
|
|
.campaign-row {
|
|
@apply flex flex-col justify-between gap-4 p-5 no-underline lg:flex-row lg:items-center;
|
|
}
|
|
|
|
.campaign-row strong {
|
|
@apply block text-xl font-black;
|
|
}
|
|
|
|
.campaign-meta {
|
|
@apply flex flex-col items-start gap-1 lg:items-end;
|
|
}
|
|
|
|
.page-message {
|
|
@apply rounded-[1.25rem] border p-4 text-sm font-medium;
|
|
background: rgba(255, 255, 255, 0.84);
|
|
border-color: var(--app-border-subtle);
|
|
color: var(--app-text-muted);
|
|
}
|
|
|
|
.page-message.error {
|
|
color: var(--app-danger-muted);
|
|
}
|
|
</style>
|