Files
social-media/frontend/src/features/clients/views/ClientsView.vue

369 lines
10 KiB
Vue

<script setup>
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import AppAvatar from '@/components/AppAvatar.vue';
import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
import { useClientsStore } from '@/features/clients/stores/clientsStore.js';
const authStore = useAuthStore();
const workspaceStore = useWorkspaceStore();
const clientsStore = useClientsStore();
const { t } = useI18n();
const isCreateFormVisible = ref(false);
const formError = ref(null);
const form = reactive({
name: '',
portraitUrl: '',
primaryContactName: '',
primaryContactEmail: '',
primaryContactPortraitUrl: '',
});
function resetForm() {
form.name = '';
form.portraitUrl = '';
form.primaryContactName = '';
form.primaryContactEmail = '';
form.primaryContactPortraitUrl = '';
formError.value = null;
}
function openCreateForm() {
resetForm();
isCreateFormVisible.value = true;
}
async function submitForm() {
if (clientsStore.isCreating) {
return;
}
formError.value = null;
if (!form.name) {
formError.value = t('clients.errors.nameRequired');
return;
}
try {
await clientsStore.createClient({
name: form.name,
portraitUrl: form.portraitUrl,
primaryContactName: form.primaryContactName,
primaryContactEmail: form.primaryContactEmail,
primaryContactPortraitUrl: form.primaryContactPortraitUrl,
});
isCreateFormVisible.value = false;
resetForm();
} catch (error) {
formError.value = t('clients.errors.createFailed');
}
}
</script>
<template>
<section class="page-shell">
<div class="header">
<div>
<div class="eyebrow">{{ t('clients.eyebrow') }}</div>
<h1>{{ t('clients.title') }}</h1>
<p>{{ t('clients.description') }}</p>
</div>
</div>
<div class="action-row">
<button
v-if="authStore.isManager"
class="create-button"
@click="openCreateForm"
>
{{ t('clients.newClient') }}
</button>
</div>
<div
v-if="isCreateFormVisible"
class="create-panel"
>
<div class="panel-header">
<strong>{{ t('clients.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.name"
class="field-wide"
:label="t('clients.fields.name')"
:disabled="clientsStore.isCreating"
variant="outlined"
hide-details
/>
<v-text-field
v-model="form.portraitUrl"
class="field-wide"
:label="t('clients.fields.portraitUrl')"
:disabled="clientsStore.isCreating"
placeholder="https://..."
type="url"
variant="outlined"
hide-details
/>
<v-text-field
v-model="form.primaryContactName"
:label="t('clients.fields.primaryContactName')"
:disabled="clientsStore.isCreating"
variant="outlined"
hide-details
/>
<v-text-field
v-model="form.primaryContactEmail"
:label="t('clients.fields.primaryContactEmail')"
:disabled="clientsStore.isCreating"
type="email"
variant="outlined"
hide-details
/>
<v-text-field
v-model="form.primaryContactPortraitUrl"
class="field-wide"
:label="t('clients.fields.primaryContactPortraitUrl')"
:disabled="clientsStore.isCreating"
placeholder="https://..."
type="url"
variant="outlined"
hide-details
/>
</div>
<div class="panel-actions">
<button
class="secondary"
:disabled="clientsStore.isCreating"
@click="isCreateFormVisible = false"
>
{{ t('common.cancel') }}
</button>
<button
class="primary"
:disabled="clientsStore.isCreating"
@click="submitForm"
>
<v-progress-circular
v-if="clientsStore.isCreating"
indeterminate
:size="16"
:width="2"
/>
<span>{{ clientsStore.isCreating ? t('common.creating') : t('clients.createTitle') }}</span>
</button>
</div>
</div>
<div
v-if="clientsStore.isLoading"
class="page-message"
>
{{ t('clients.loading') }}
</div>
<div
v-else-if="clientsStore.error"
class="page-message error"
>
{{ clientsStore.error }}
</div>
<div class="grid-list">
<router-link
v-for="client in clientsStore.clients"
:key="client.id"
:to="{ name: 'client-detail', params: { clientId: client.id } }"
class="client-card"
>
<div class="client-card-header">
<div>
<strong>{{ client.name }}</strong>
<span>{{ client.status }}</span>
</div>
<AppAvatar
:name="client.name"
:src="client.portraitUrl"
/>
</div>
<em>{{ client.primaryContactName || t('clients.noPrimaryContact') }}</em>
<small>{{ client.primaryContactEmail || t('clients.noPrimaryContactEmail') }}</small>
</router-link>
</div>
<div
v-if="!clientsStore.isLoading && !clientsStore.clients.length"
class="page-message"
>
{{ t('clients.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: #0f766e;
}
.header h1 {
@apply mt-2 text-4xl font-black;
color: #172033;
}
.header p {
@apply mt-2 text-sm leading-6;
color: #526178;
}
.header {
@apply flex flex-col gap-3;
}
.action-row {
@apply flex justify-start;
}
.create-button,
.primary,
.secondary {
@apply rounded-full px-5 py-3 text-sm font-bold transition;
}
.primary,
.secondary {
@apply inline-flex items-center justify-center gap-2;
}
.create-button,
.primary {
background: #172033;
color: white;
}
.create-button:hover,
.primary:hover {
background: #0f172a;
}
.secondary {
background: rgba(255, 255, 255, 0.84);
border: 1px solid rgba(23, 32, 51, 0.12);
color: #172033;
}
.create-panel {
@apply flex flex-col gap-5 rounded-[1.75rem] border p-5 md:p-6;
background: rgba(255, 255, 255, 0.92);
border-color: rgba(23, 32, 51, 0.08);
}
.panel-header {
@apply flex flex-col gap-1 md:flex-row md:items-center md:justify-between;
}
.panel-header strong {
@apply text-lg font-black;
color: #172033;
}
.panel-header span {
@apply text-sm font-semibold;
color: #526178;
}
.form-grid {
@apply grid gap-4 md:grid-cols-2;
}
.field {
@apply flex flex-col gap-2 text-sm font-semibold;
color: #172033;
}
.field.field-wide {
@apply md:col-span-2;
}
.field input {
@apply rounded-2xl border px-4 py-3 text-sm;
border-color: rgba(23, 32, 51, 0.12);
background: white;
color: #172033;
}
.panel-actions {
@apply flex flex-col gap-3 sm:flex-row sm:justify-end;
}
.grid-list {
@apply grid gap-4 md:grid-cols-2 xl:grid-cols-3;
}
.page-message {
@apply rounded-[1.25rem] border p-4 text-sm font-medium;
background: rgba(255, 255, 255, 0.84);
border-color: rgba(23, 32, 51, 0.08);
color: #526178;
}
.page-message.error {
color: #b91c1c;
}
.client-card {
@apply flex flex-col gap-3 rounded-[1.5rem] border p-5;
background: rgba(255, 255, 255, 0.84);
border-color: rgba(23, 32, 51, 0.08);
text-decoration: none;
}
.client-card-header {
@apply flex items-start justify-between gap-4;
}
.client-card strong {
@apply text-xl font-black;
color: #172033;
}
.client-card span {
@apply text-sm font-semibold uppercase tracking-[0.18em];
color: #ff8a3d;
}
.client-card em {
@apply text-sm leading-6 not-italic;
color: #526178;
}
.client-card small {
@apply text-xs leading-5;
color: #7b8798;
}
</style>