369 lines
10 KiB
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>
|