Merge design_ui_v2 into design_ui so the new design system can be previewed without dropping later product work.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -90,7 +90,7 @@ form {
|
|||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
padding: 0.35rem 0.4rem 0.35rem 0.7rem;
|
padding: 0.35rem 0.4rem 0.35rem 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ input {
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
background: #14b8a6;
|
background: #14b8a6;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -126,7 +126,7 @@ button:disabled {
|
|||||||
|
|
||||||
.answer {
|
.answer {
|
||||||
background: #f0fdfa;
|
background: #f0fdfa;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.7rem 0.85rem;
|
padding: 0.7rem 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||||
|
import { UiField, UiInput, UiSelect } from '@/components/ui';
|
||||||
import type {
|
import type {
|
||||||
CustomFieldDefinitionDto,
|
CustomFieldDefinitionDto,
|
||||||
CustomFieldEntity,
|
CustomFieldEntity,
|
||||||
@@ -58,27 +59,28 @@ onMounted(load);
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="loading" class="muted">بارگذاری فیلدهای سفارشی…</div>
|
<div v-if="loading" class="muted">بارگذاری فیلدهای سفارشی…</div>
|
||||||
<div v-else-if="fields.length" class="custom-block">
|
<div v-else-if="fields.length" class="custom-block">
|
||||||
<h3>فیلدهای سفارشی</h3>
|
<h3 class="custom-title">فیلدهای سفارشی</h3>
|
||||||
<div class="grid">
|
<div class="crm-form-grid-2">
|
||||||
<div v-for="field in fields" :key="field.id" class="field">
|
<UiField
|
||||||
<label :for="`cf-${field.key}`">
|
v-for="field in fields"
|
||||||
{{ field.label }}
|
:key="field.id"
|
||||||
<span v-if="field.required" class="req">*</span>
|
:label="field.label"
|
||||||
</label>
|
:required="field.required"
|
||||||
|
:for-id="`cf-${field.key}`"
|
||||||
<select
|
>
|
||||||
|
<UiSelect
|
||||||
v-if="field.fieldType === 'select'"
|
v-if="field.fieldType === 'select'"
|
||||||
:id="`cf-${field.key}`"
|
:id="`cf-${field.key}`"
|
||||||
:value="String(displayValue(field.key))"
|
:model-value="String(displayValue(field.key))"
|
||||||
@change="setValue(field.key, ($event.target as HTMLSelectElement).value)"
|
@update:model-value="setValue(field.key, $event)"
|
||||||
>
|
>
|
||||||
<option value="">انتخاب کنید</option>
|
<option value="">انتخاب کنید</option>
|
||||||
<option v-for="opt in field.options" :key="opt" :value="opt">
|
<option v-for="opt in field.options" :key="opt" :value="opt">
|
||||||
{{ opt }}
|
{{ opt }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
|
|
||||||
<label v-else-if="field.fieldType === 'boolean'" class="check">
|
<label v-else-if="field.fieldType === 'boolean'" class="cf-check">
|
||||||
<input
|
<input
|
||||||
:id="`cf-${field.key}`"
|
:id="`cf-${field.key}`"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -87,20 +89,18 @@ onMounted(load);
|
|||||||
setValue(field.key, ($event.target as HTMLInputElement).checked)
|
setValue(field.key, ($event.target as HTMLInputElement).checked)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
فعال
|
<span>فعال</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<UiInput
|
||||||
v-else-if="field.fieldType === 'number'"
|
v-else-if="field.fieldType === 'number'"
|
||||||
:id="`cf-${field.key}`"
|
:id="`cf-${field.key}`"
|
||||||
type="number"
|
type="number"
|
||||||
:value="String(displayValue(field.key))"
|
:model-value="props.modelValue[field.key] != null ? Number(props.modelValue[field.key]) : ''"
|
||||||
@input="
|
@update:model-value="
|
||||||
setValue(
|
setValue(
|
||||||
field.key,
|
field.key,
|
||||||
($event.target as HTMLInputElement).value === ''
|
$event === '' || $event == null ? null : Number($event),
|
||||||
? null
|
|
||||||
: Number(($event.target as HTMLInputElement).value),
|
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -112,66 +112,55 @@ onMounted(load);
|
|||||||
@update:model-value="setValue(field.key, $event || null)"
|
@update:model-value="setValue(field.key, $event || null)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<input
|
<UiInput
|
||||||
v-else
|
v-else
|
||||||
:id="`cf-${field.key}`"
|
:id="`cf-${field.key}`"
|
||||||
type="text"
|
type="text"
|
||||||
:value="String(displayValue(field.key))"
|
:model-value="String(displayValue(field.key))"
|
||||||
@input="setValue(field.key, ($event.target as HTMLInputElement).value)"
|
@update:model-value="setValue(field.key, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</UiField>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.custom-block {
|
.custom-block {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 0.75rem;
|
flex-direction: column;
|
||||||
padding-top: 0.4rem;
|
gap: var(--sp-3);
|
||||||
|
padding-top: var(--sp-4);
|
||||||
border-top: 1px dashed var(--line);
|
border-top: 1px dashed var(--line);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
.custom-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: var(--fs-sm);
|
||||||
}
|
font-weight: var(--fw-semibold);
|
||||||
|
color: var(--ink-secondary);
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.req {
|
.cf-check {
|
||||||
color: #b42318;
|
display: inline-flex;
|
||||||
}
|
|
||||||
|
|
||||||
.check {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: var(--sp-2);
|
||||||
font-weight: 500;
|
min-height: var(--control-h-md);
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
color: var(--ink);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
select,
|
.cf-check input[type='checkbox'] {
|
||||||
input[type='text'],
|
width: 18px;
|
||||||
input[type='number'] {
|
height: 18px;
|
||||||
border: 1px solid var(--line);
|
accent-color: var(--accent);
|
||||||
border-radius: 12px;
|
cursor: pointer;
|
||||||
padding: 0.8rem 0.95rem;
|
|
||||||
background: #fff;
|
|
||||||
font: inherit;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ onMounted(load);
|
|||||||
.composer input {
|
.composer input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.7rem 0.85rem;
|
padding: 0.7rem 0.85rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@@ -316,7 +316,7 @@ onMounted(load);
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.75rem 0.85rem;
|
padding: 0.75rem 0.85rem;
|
||||||
background: #fbfcfb;
|
background: #fbfcfb;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -675,7 +675,7 @@ onBeforeUnmount(() => {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.45rem 0.85rem 0.45rem 0.45rem;
|
padding: 0.45rem 0.85rem 0.45rem 0.45rem;
|
||||||
background: linear-gradient(180deg, #ffffff 0%, #f8fffd 100%);
|
background: linear-gradient(180deg, #ffffff 0%, #f8fffd 100%);
|
||||||
}
|
}
|
||||||
@@ -698,7 +698,7 @@ onBeforeUnmount(() => {
|
|||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: 50%;
|
bottom: 50%;
|
||||||
transform-origin: center bottom;
|
transform-origin: center bottom;
|
||||||
border-radius: 99px;
|
border-radius: var(--radius);
|
||||||
background: #0f766e;
|
background: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +726,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.clock-text {
|
.clock-text {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
@@ -768,7 +768,7 @@ onBeforeUnmount(() => {
|
|||||||
max-height: calc(100vh - 16px);
|
max-height: calc(100vh - 16px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
||||||
padding: 0.75rem 0.8rem 0.7rem;
|
padding: 0.75rem 0.8rem 0.7rem;
|
||||||
font-family: IRANSans, Tahoma, sans-serif;
|
font-family: IRANSans, Tahoma, sans-serif;
|
||||||
@@ -797,7 +797,7 @@ onBeforeUnmount(() => {
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -826,7 +826,7 @@ onBeforeUnmount(() => {
|
|||||||
.day {
|
.day {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 9px;
|
border-radius: var(--radius);
|
||||||
background: none;
|
background: none;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -881,12 +881,12 @@ onBeforeUnmount(() => {
|
|||||||
min-width: 5.1rem;
|
min-width: 5.1rem;
|
||||||
height: 4.1rem;
|
height: 4.1rem;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 2.35rem;
|
font-size: 2.35rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -900,7 +900,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.md-colon {
|
.md-colon {
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding-bottom: 0.15rem;
|
padding-bottom: 0.15rem;
|
||||||
@@ -1005,7 +1005,7 @@ onBeforeUnmount(() => {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
@@ -1033,7 +1033,7 @@ onBeforeUnmount(() => {
|
|||||||
height: 220px;
|
height: 220px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
@@ -1044,7 +1044,7 @@ onBeforeUnmount(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -1087,9 +1087,9 @@ onBeforeUnmount(() => {
|
|||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
background: #14b8a6;
|
background: #14b8a6;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.4rem 0.8rem;
|
padding: 0.4rem 0.8rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ h2 {
|
|||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.75rem 0.9rem;
|
padding: 0.75rem 0.9rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -221,7 +221,7 @@ textarea {
|
|||||||
|
|
||||||
.items li {
|
.items li {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.75rem 0.85rem;
|
padding: 0.75rem 0.85rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ function formatPayloadSummary(value: NoteAiPayloadDto | null) {
|
|||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
padding: 0.75rem 0.85rem;
|
padding: 0.75rem 0.85rem;
|
||||||
border: 1px dashed var(--accent);
|
border: 1px dashed var(--accent);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: rgba(255, 255, 255, 0.65);
|
background: rgba(255, 255, 255, 0.65);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
@@ -281,7 +281,7 @@ header {
|
|||||||
input[type='text'],
|
input[type='text'],
|
||||||
textarea {
|
textarea {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.55rem 0.7rem;
|
padding: 0.55rem 0.7rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import type { RouteLocationRaw } from 'vue-router';
|
|||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
|
||||||
const FAB_SIZE = 36;
|
const FAB_SIZE = 36;
|
||||||
const FAB_MARGIN = 20;
|
const FAB_MARGIN_BOTTOM = 24;
|
||||||
const POS_KEY = 'mento.quickCreateFab';
|
const FAB_MARGIN_LEFT = 24;
|
||||||
|
const POS_KEY = 'mento.quickCreateFab_v2';
|
||||||
|
|
||||||
type FabPos = { left: number; top: number };
|
type FabPos = { left: number; top: number };
|
||||||
|
|
||||||
@@ -67,7 +68,10 @@ const router = useRouter();
|
|||||||
const hidden = computed(() => route.name === 'person-detail');
|
const hidden = computed(() => route.name === 'person-detail');
|
||||||
const isQuickCreateOpen = ref(false);
|
const isQuickCreateOpen = ref(false);
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
const fabPos = ref<FabPos>({ left: FAB_MARGIN, top: FAB_MARGIN });
|
const fabPos = ref<FabPos>({
|
||||||
|
left: FAB_MARGIN_LEFT,
|
||||||
|
top: typeof window !== 'undefined' ? window.innerHeight - FAB_MARGIN_BOTTOM - FAB_SIZE : 600,
|
||||||
|
});
|
||||||
const viewport = ref({ w: 1280, h: 800 });
|
const viewport = ref({ w: 1280, h: 800 });
|
||||||
const dragging = ref(false);
|
const dragging = ref(false);
|
||||||
let dragMoved = false;
|
let dragMoved = false;
|
||||||
@@ -75,10 +79,10 @@ let dragOffsetX = 0;
|
|||||||
let dragOffsetY = 0;
|
let dragOffsetY = 0;
|
||||||
|
|
||||||
function defaultPos(): FabPos {
|
function defaultPos(): FabPos {
|
||||||
const bottomGap = viewport.value.w <= 900 ? FAB_MARGIN + 64 : FAB_MARGIN;
|
const extra = viewport.value.w <= 900 ? 64 : 0;
|
||||||
return {
|
return {
|
||||||
left: FAB_MARGIN,
|
left: FAB_MARGIN_LEFT,
|
||||||
top: Math.max(FAB_MARGIN, viewport.value.h - bottomGap - FAB_SIZE),
|
top: Math.max(8, viewport.value.h - FAB_MARGIN_BOTTOM - extra - FAB_SIZE),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +175,7 @@ const fabStyle = computed(() => ({
|
|||||||
const panelStyle = computed(() => {
|
const panelStyle = computed(() => {
|
||||||
const gap = 12;
|
const gap = 12;
|
||||||
const edge = 16;
|
const edge = 16;
|
||||||
const width = Math.min(390, viewport.value.w - 32);
|
const width = Math.min(300, viewport.value.w - 32);
|
||||||
let left = fabPos.value.left;
|
let left = fabPos.value.left;
|
||||||
left = Math.min(left, viewport.value.w - width - edge);
|
left = Math.min(left, viewport.value.w - width - edge);
|
||||||
left = Math.max(edge, left);
|
left = Math.max(edge, left);
|
||||||
@@ -278,7 +282,7 @@ onBeforeUnmount(() => {
|
|||||||
@click="go(item.to)"
|
@click="go(item.to)"
|
||||||
>
|
>
|
||||||
<span class="item-ico" :class="item.tone">
|
<span class="item-ico" :class="item.tone">
|
||||||
<UiIcon :name="item.icon" :size="26" />
|
<UiIcon :name="item.icon" :size="18" />
|
||||||
</span>
|
</span>
|
||||||
<span class="copy">
|
<span class="copy">
|
||||||
<strong>{{ item.title }}</strong>
|
<strong>{{ item.title }}</strong>
|
||||||
@@ -303,7 +307,7 @@ onBeforeUnmount(() => {
|
|||||||
@pointercancel="onFabPointerUp"
|
@pointercancel="onFabPointerUp"
|
||||||
>
|
>
|
||||||
<span class="fab-face" aria-hidden="true">
|
<span class="fab-face" aria-hidden="true">
|
||||||
<UiIcon :name="isQuickCreateOpen ? 'x' : 'apps-plus'" :size="28" />
|
<UiIcon :name="isQuickCreateOpen ? 'x' : 'apps-plus'" :size="20" />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,25 +332,26 @@ onBeforeUnmount(() => {
|
|||||||
.panel {
|
.panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 92;
|
z-index: 92;
|
||||||
width: 390px;
|
width: 300px;
|
||||||
max-height: min(540px, calc(100dvh - 80px));
|
max-height: min(480px, calc(100dvh - 80px));
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 28px;
|
border: 1px solid var(--line);
|
||||||
padding: 16px 16px 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
padding: 12px 12px 10px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel h2 {
|
.panel h2 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin: 0 0 12px;
|
margin: 0 0 10px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 20px;
|
font-size: var(--fs-md);
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
color: #111827;
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
@@ -354,31 +359,37 @@ onBeforeUnmount(() => {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 68px;
|
min-height: 46px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 10px;
|
||||||
padding: 10px 14px;
|
padding: 6px 10px;
|
||||||
border: 1px solid #eef1f2;
|
border: 1px solid var(--line-light);
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.04);
|
box-shadow: var(--shadow-xs);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-ico {
|
.item-ico {
|
||||||
width: 52px;
|
width: 34px;
|
||||||
height: 52px;
|
height: 34px;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -390,7 +401,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-ico.company {
|
.item-ico.company {
|
||||||
background: #22d3ee;
|
background: #0891b2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-ico.deal {
|
.item-ico.deal {
|
||||||
@@ -398,7 +409,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-ico.activity {
|
.item-ico.activity {
|
||||||
background: #f97316;
|
background: #ea580c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-ico.note {
|
.item-ico.note {
|
||||||
@@ -407,33 +418,34 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.copy {
|
.copy {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy strong {
|
.copy strong {
|
||||||
font-size: 16px;
|
font-size: var(--fs-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #111827;
|
color: var(--ink);
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy small {
|
.copy small {
|
||||||
font-size: 13px;
|
font-size: var(--fs-xs);
|
||||||
color: #94a3b8;
|
color: var(--muted);
|
||||||
line-height: 1.5;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fab {
|
.fab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 93;
|
z-index: 93;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 36px;
|
width: var(--control-h);
|
||||||
height: 36px;
|
height: var(--control-h);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(145deg, #0f766e 0%, #14b8a6 100%);
|
background: linear-gradient(145deg, #0f766e 0%, #14b8a6 100%);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -442,7 +454,7 @@ onBeforeUnmount(() => {
|
|||||||
line-height: 0;
|
line-height: 0;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 22px rgba(13, 148, 136, 0.4),
|
0 4px 14px rgba(13, 148, 136, 0.35),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
@@ -463,29 +475,28 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.fab :deep(.ico) {
|
.fab :deep(.ico) {
|
||||||
display: block;
|
display: block;
|
||||||
width: 28px;
|
width: 20px;
|
||||||
height: 28px;
|
height: 20px;
|
||||||
max-width: 28px;
|
max-width: 20px;
|
||||||
max-height: 28px;
|
max-height: 20px;
|
||||||
stroke-width: 1.7;
|
stroke-width: 1.7;
|
||||||
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.15));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fab:hover {
|
.fab:hover {
|
||||||
filter: brightness(1.06);
|
filter: brightness(1.06);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 10px 26px rgba(13, 148, 136, 0.48),
|
0 6px 18px rgba(13, 148, 136, 0.42),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fab.open {
|
.fab.open {
|
||||||
background: linear-gradient(145deg, #115e59 0%, #0d9488 100%);
|
background: linear-gradient(145deg, #115e59 0%, #0d9488 100%);
|
||||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.28);
|
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fab.dragging {
|
.fab.dragging {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
box-shadow: 0 12px 28px rgba(13, 148, 136, 0.5);
|
box-shadow: 0 8px 24px rgba(13, 148, 136, 0.45);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -64,7 +64,13 @@ defineProps<{
|
|||||||
| 'logout'
|
| 'logout'
|
||||||
| 'pulse'
|
| 'pulse'
|
||||||
| 'sliders'
|
| 'sliders'
|
||||||
| 'camera';
|
| 'camera'
|
||||||
|
| 'arrow-right'
|
||||||
|
| 'arrow-left'
|
||||||
|
| 'chevron-right'
|
||||||
|
| 'chevron-left'
|
||||||
|
| 'chevrons-right'
|
||||||
|
| 'chevrons-left';
|
||||||
size?: number;
|
size?: number;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@@ -218,9 +224,24 @@ defineProps<{
|
|||||||
<template v-else-if="name === 'plus'">
|
<template v-else-if="name === 'plus'">
|
||||||
<path d="M12 6v12M6 12h12" />
|
<path d="M12 6v12M6 12h12" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="name === 'chevron'">
|
<template v-else-if="name === 'chevron' || name === 'chevron-right'">
|
||||||
<path d="m9 6 6 6-6 6" />
|
<path d="m9 6 6 6-6 6" />
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="name === 'chevron-left'">
|
||||||
|
<path d="m15 18-6-6 6-6" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="name === 'chevrons-right'">
|
||||||
|
<path d="m6 17 5-5-5-5M13 17l5-5-5-5" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="name === 'chevrons-left'">
|
||||||
|
<path d="m11 17-5-5 5-5M18 17l-5-5 5-5" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="name === 'arrow-right'">
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="name === 'arrow-left'">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</template>
|
||||||
<template v-else-if="name === 'star'">
|
<template v-else-if="name === 'star'">
|
||||||
<path d="m12 3.4 2.4 4.9 5.4.8-3.9 3.8.9 5.4L12 15.8 7.2 18.3l.9-5.4-3.9-3.8 5.4-.8L12 3.4Z" />
|
<path d="m12 3.4 2.4 4.9 5.4.8-3.9 3.8.9 5.4L12 15.8 7.2 18.3l.9-5.4-3.9-3.8 5.4-.8L12 3.4Z" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ img {
|
|||||||
em {
|
em {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
font-size: 0.62rem;
|
font-size: 0.62rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ onBeforeUnmount(() => {
|
|||||||
.meter i {
|
.meter i {
|
||||||
display: block;
|
display: block;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
border-radius: 99px;
|
border-radius: var(--radius);
|
||||||
background: #5eead4;
|
background: #5eead4;
|
||||||
box-shadow: 0 0 8px rgba(94, 234, 212, 0.45);
|
box-shadow: 0 0 8px rgba(94, 234, 212, 0.45);
|
||||||
transition: height 70ms linear;
|
transition: height 70ms linear;
|
||||||
@@ -402,7 +402,7 @@ onBeforeUnmount(() => {
|
|||||||
height: 38px;
|
height: 38px;
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -296,8 +296,8 @@ onBeforeUnmount(() => {
|
|||||||
.head h2 {
|
.head h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.05rem;
|
font-size: 16px;
|
||||||
font-weight: 800;
|
font-weight: 500;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ onBeforeUnmount(() => {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 0.9rem 0.95rem;
|
padding: 0.9rem 0.95rem;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ onBeforeUnmount(() => {
|
|||||||
.ctx-copy h3 {
|
.ctx-copy h3 {
|
||||||
margin: 0.1rem 0 0;
|
margin: 0.1rem 0 0;
|
||||||
font-size: 0.98rem;
|
font-size: 0.98rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
min-height: 11.5rem;
|
min-height: 11.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -448,13 +448,13 @@ onBeforeUnmount(() => {
|
|||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +473,7 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 0.74rem;
|
font-size: 0.74rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 0.42rem 0.65rem;
|
padding: 0.42rem 0.65rem;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +497,7 @@ onBeforeUnmount(() => {
|
|||||||
max-height: 12rem;
|
max-height: 12rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.16);
|
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.16);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,7 @@ onBeforeUnmount(() => {
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
padding: 0.55rem 0.65rem;
|
padding: 0.55rem 0.65rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -536,7 +536,7 @@ onBeforeUnmount(() => {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.72rem 0.8rem;
|
padding: 0.72rem 0.8rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #ecfdf5;
|
background: #ecfdf5;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@@ -559,7 +559,7 @@ onBeforeUnmount(() => {
|
|||||||
height: var(--control-h);
|
height: var(--control-h);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,7 +600,7 @@ onBeforeUnmount(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
height: min(92dvh, 860px);
|
height: min(92dvh, 860px);
|
||||||
border-radius: 22px;
|
border-radius: var(--radius, 10px);
|
||||||
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22);
|
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -274,8 +274,8 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.titles h2 {
|
.titles h2 {
|
||||||
font-size: 1.02rem;
|
font-size: 16px;
|
||||||
font-weight: 800;
|
font-weight: 500;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ onBeforeUnmount(() => {
|
|||||||
min-height: 72px;
|
min-height: 72px;
|
||||||
padding: 0.7rem 0.9rem;
|
padding: 0.7rem 0.9rem;
|
||||||
border: 1px solid #e8eef3;
|
border: 1px solid #e8eef3;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
@@ -340,7 +340,7 @@ onBeforeUnmount(() => {
|
|||||||
.label,
|
.label,
|
||||||
.copy strong {
|
.copy strong {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ onBeforeUnmount(() => {
|
|||||||
.ico-box {
|
.ico-box {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
background: #e6f7f4;
|
background: #e6f7f4;
|
||||||
color: #0d9488;
|
color: #0d9488;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -375,13 +375,13 @@ onBeforeUnmount(() => {
|
|||||||
.when {
|
.when {
|
||||||
color: #0d9488;
|
color: #0d9488;
|
||||||
font-size: 0.74rem;
|
font-size: 0.74rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-box {
|
.custom-box {
|
||||||
border: 1px solid #e8eef3;
|
border: 1px solid #e8eef3;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.85rem 0.9rem;
|
padding: 0.85rem 0.9rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
@@ -419,7 +419,7 @@ onBeforeUnmount(() => {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
padding: 0.85rem 0.9rem;
|
padding: 0.85rem 0.9rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@@ -452,7 +452,7 @@ onBeforeUnmount(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
height: min(92dvh, 860px);
|
height: min(92dvh, 860px);
|
||||||
border-radius: 22px;
|
border-radius: var(--radius, 10px);
|
||||||
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22);
|
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.22);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ withDefaults(
|
|||||||
.brand-copy strong {
|
.brand-copy strong {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-copy span {
|
.brand-copy span {
|
||||||
@@ -117,7 +117,7 @@ withDefaults(
|
|||||||
aspect-ratio: 1.35;
|
aspect-ratio: 1.35;
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: rgba(255, 255, 255, 0.88);
|
||||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.1);
|
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.1);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
@@ -150,7 +150,7 @@ withDefaults(
|
|||||||
|
|
||||||
.chart-line {
|
.chart-line {
|
||||||
height: 54px;
|
height: 54px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, transparent 55%, rgba(13, 148, 136, 0.12) 55%),
|
linear-gradient(180deg, transparent 55%, rgba(13, 148, 136, 0.12) 55%),
|
||||||
radial-gradient(circle at 20% 60%, #14b8a6 3px, transparent 4px),
|
radial-gradient(circle at 20% 60%, #14b8a6 3px, transparent 4px),
|
||||||
@@ -184,7 +184,7 @@ withDefaults(
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.1);
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ function nodeClass(step: AutomationStep) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 22rem;
|
min-height: 22rem;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: #f3f6f5;
|
background-color: #f3f6f5;
|
||||||
background-image: radial-gradient(#c9d4cf 1.1px, transparent 1.2px);
|
background-image: radial-gradient(#c9d4cf 1.1px, transparent 1.2px);
|
||||||
@@ -254,7 +254,7 @@ function nodeClass(step: AutomationStep) {
|
|||||||
gap: 0.7rem;
|
gap: 0.7rem;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
padding: 0.72rem 0.8rem;
|
padding: 0.72rem 0.8rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
border: 1px solid #d5ddd8;
|
border: 1px solid #d5ddd8;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 10px 24px rgba(23, 33, 28, 0.06);
|
box-shadow: 0 10px 24px rgba(23, 33, 28, 0.06);
|
||||||
@@ -273,7 +273,7 @@ function nodeClass(step: AutomationStep) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.node.action:not(.condition) {
|
.node.action:not(.condition) {
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
border-inline-start-width: 5px;
|
border-inline-start-width: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ function nodeClass(step: AutomationStep) {
|
|||||||
.icon {
|
.icon {
|
||||||
width: 2.35rem;
|
width: 2.35rem;
|
||||||
height: 2.35rem;
|
height: 2.35rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -350,7 +350,7 @@ function nodeClass(step: AutomationStep) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.node.condition .icon {
|
.node.condition .icon {
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
background: #ffedd5;
|
background: #ffedd5;
|
||||||
}
|
}
|
||||||
@@ -369,7 +369,7 @@ function nodeClass(step: AutomationStep) {
|
|||||||
|
|
||||||
strong {
|
strong {
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +472,7 @@ em {
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #d5ddd8;
|
border: 1px solid #d5ddd8;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
|
|||||||
@@ -278,14 +278,14 @@ watch(
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.98rem;
|
font-size: 0.98rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
@@ -312,7 +312,7 @@ input,
|
|||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.55rem 0.7rem;
|
padding: 0.55rem 0.7rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ withDefaults(
|
|||||||
width: max-content;
|
width: max-content;
|
||||||
max-width: 118px;
|
max-width: 118px;
|
||||||
padding: 0.48rem 0.65rem 0.55rem;
|
padding: 0.48rem 0.65rem 0.55rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ withDefaults(
|
|||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text small {
|
.text small {
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ function pick(label: string) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.panel {
|
.panel {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 20px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem 1rem 0.95rem;
|
padding: 1rem 1rem 0.95rem;
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
border: 1px solid #eef2f7;
|
border: 1px solid #eef2f7;
|
||||||
@@ -144,7 +144,7 @@ h2 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
padding: 0.58rem 0.7rem;
|
padding: 0.58rem 0.7rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #eef2f7;
|
border: 1px solid #eef2f7;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
@@ -168,7 +168,7 @@ h2 {
|
|||||||
.chip-ico {
|
.chip-ico {
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
background: #ecfdf8;
|
background: #ecfdf8;
|
||||||
color: #14b8a6;
|
color: #14b8a6;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -185,7 +185,7 @@ h2 {
|
|||||||
.ai-input input {
|
.ai-input input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.58rem 0.75rem;
|
padding: 0.58rem 0.75rem;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
@@ -194,7 +194,7 @@ h2 {
|
|||||||
.ai-input button {
|
.ai-input button {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #14b8a6;
|
background: #14b8a6;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -208,7 +208,7 @@ h2 {
|
|||||||
|
|
||||||
.ai-answer {
|
.ai-answer {
|
||||||
background: #f0fdfa;
|
background: #f0fdfa;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.7rem 0.8rem;
|
padding: 0.7rem 0.8rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ function alertIcon(tone: string) {
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0.8rem 1.5rem 0.9rem;
|
padding: 0.8rem 1.5rem 0.9rem;
|
||||||
border-radius: 28px;
|
border-radius: var(--radius);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -151,7 +151,7 @@ function alertIcon(tone: string) {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
border-radius: 28px;
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #134e4a;
|
background-color: #134e4a;
|
||||||
background-image:
|
background-image:
|
||||||
@@ -251,7 +251,7 @@ function alertIcon(tone: string) {
|
|||||||
.greet-text h1 {
|
.greet-text h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: clamp(1.35rem, 2vw, 1.75rem);
|
font-size: clamp(1.35rem, 2vw, 1.75rem);
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,7 @@ function alertIcon(tone: string) {
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0.4rem 0.5rem;
|
padding: 0.4rem 0.5rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -311,7 +311,7 @@ function alertIcon(tone: string) {
|
|||||||
.chip-ico {
|
.chip-ico {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -369,7 +369,7 @@ function alertIcon(tone: string) {
|
|||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 7px;
|
border-radius: var(--radius);
|
||||||
padding: 0.14rem 0.38rem;
|
padding: 0.14rem 0.38rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const iconTones: Record<string, string> = {
|
|||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 22px;
|
border-radius: var(--radius);
|
||||||
padding: 1.1rem 1.15rem;
|
padding: 1.1rem 1.15rem;
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
border: 1px solid #eef2f7;
|
border: 1px solid #eef2f7;
|
||||||
@@ -67,7 +67,7 @@ const iconTones: Record<string, string> = {
|
|||||||
.kpi-icon {
|
.kpi-icon {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const cols = computed<Col[]>(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.panel {
|
.panel {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 20px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem 1.05rem;
|
padding: 1rem 1.05rem;
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
border: 1px solid #eef2f7;
|
border: 1px solid #eef2f7;
|
||||||
@@ -151,7 +151,7 @@ a {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.55rem 0.6rem;
|
padding: 0.55rem 0.6rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border: 1px solid #eef2f7;
|
border: 1px solid #eef2f7;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -185,12 +185,12 @@ a {
|
|||||||
.deal-mark {
|
.deal-mark {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
background: #ecfdf8;
|
background: #ecfdf8;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const rows = computed<Row[]>(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.panel {
|
.panel {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 20px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem 1.05rem;
|
padding: 1rem 1.05rem;
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
border: 1px solid #eef2f7;
|
border: 1px solid #eef2f7;
|
||||||
@@ -151,7 +151,7 @@ a {
|
|||||||
.act-icon {
|
.act-icon {
|
||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #ecfdf8;
|
background: #ecfdf8;
|
||||||
color: #14b8a6;
|
color: #14b8a6;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function trophy(rank: number) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.panel {
|
.panel {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 20px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem 1.05rem;
|
padding: 1rem 1.05rem;
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
border: 1px solid #eef2f7;
|
border: 1px solid #eef2f7;
|
||||||
@@ -89,12 +89,12 @@ a {
|
|||||||
.cust-avatar {
|
.cust-avatar {
|
||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: linear-gradient(145deg, #ccfbf1, #99f6e4);
|
background: linear-gradient(145deg, #ccfbf1, #99f6e4);
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -775,7 +775,7 @@ onMounted(async () => {
|
|||||||
gap: 0.28rem;
|
gap: 0.28rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0.8rem 0.9rem;
|
padding: 0.8rem 0.9rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
@@ -789,7 +789,7 @@ onMounted(async () => {
|
|||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.balance-box em {
|
.balance-box em {
|
||||||
@@ -843,7 +843,7 @@ onMounted(async () => {
|
|||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.45rem 0.75rem;
|
padding: 0.45rem 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -918,7 +918,7 @@ onMounted(async () => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.3rem 0.4rem;
|
padding: 0.3rem 0.4rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
@@ -942,21 +942,21 @@ onMounted(async () => {
|
|||||||
color: #92400e;
|
color: #92400e;
|
||||||
background: #fffbeb;
|
background: #fffbeb;
|
||||||
border: 1px solid #fde68a;
|
border: 1px solid #fde68a;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
padding: 0.55rem 0.7rem;
|
padding: 0.55rem 0.7rem;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.45rem 0.6rem;
|
padding: 0.45rem 0.6rem;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.45rem 0.8rem;
|
padding: 0.45rem 0.8rem;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ withDefaults(
|
|||||||
h3 {
|
h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.08rem;
|
font-size: 1.08rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #1e2937;
|
color: #1e2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ function onSelect(key: string) {
|
|||||||
height: 38px;
|
height: 38px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
@@ -175,7 +175,7 @@ function onSelect(key: string) {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.16);
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.16);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -193,7 +193,7 @@ function onSelect(key: string) {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ const colors = computed(() => PIPELINE_ACCENT[props.accent]);
|
|||||||
background: var(--badge-bg);
|
background: var(--badge-bg);
|
||||||
color: var(--badge-fg);
|
color: var(--badge-fg);
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ const colors = computed(() => PIPELINE_ACCENT[props.accent]);
|
|||||||
.titles h3 {
|
.titles h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -230,7 +230,7 @@ const colors = computed(() => PIPELINE_ACCENT[props.accent]);
|
|||||||
|
|
||||||
.amount strong {
|
.amount strong {
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
@@ -295,10 +295,10 @@ onBeforeUnmount(() => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 0.98rem;
|
font-size: 0.98rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-head h2 {
|
.column-head h2 {
|
||||||
@@ -315,7 +315,7 @@ onBeforeUnmount(() => {
|
|||||||
height: 1.7rem;
|
height: 1.7rem;
|
||||||
padding: 0 0.4rem;
|
padding: 0 0.4rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +389,7 @@ onBeforeUnmount(() => {
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-menu button:hover:not(:disabled) {
|
.col-menu button:hover:not(:disabled) {
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.titles {
|
.titles {
|
||||||
@@ -244,7 +244,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
|||||||
|
|
||||||
.titles strong {
|
.titles strong {
|
||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -285,7 +285,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
|||||||
}
|
}
|
||||||
|
|
||||||
.amount strong {
|
.amount strong {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
|||||||
.card {
|
.card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
|
border: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.95rem 1rem;
|
padding: 0.95rem 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.7rem;
|
gap: 0.7rem;
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function onPick(value: string) {
|
|||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
variant?: 'neutral' | 'accent' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
dot?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
variant: 'neutral',
|
||||||
|
size: 'md',
|
||||||
|
dot: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="ui-badge" :class="[variant, size]">
|
||||||
|
<span v-if="dot" class="badge-dot" aria-hidden="true" />
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ui-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge.sm {
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
.ui-badge.neutral {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
color: var(--ink-secondary);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge.accent,
|
||||||
|
.ui-badge.primary {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-deep);
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge.success {
|
||||||
|
background: var(--success-soft);
|
||||||
|
color: var(--success-deep);
|
||||||
|
border-color: color-mix(in srgb, var(--success) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge.warning {
|
||||||
|
background: var(--warning-soft);
|
||||||
|
color: var(--warning-deep);
|
||||||
|
border-color: color-mix(in srgb, var(--warning) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge.danger {
|
||||||
|
background: var(--danger-soft);
|
||||||
|
color: var(--danger);
|
||||||
|
border-color: color-mix(in srgb, var(--danger) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge.info {
|
||||||
|
background: var(--info-soft);
|
||||||
|
color: var(--info-deep);
|
||||||
|
border-color: color-mix(in srgb, var(--info) 30%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
variant?: 'primary' | 'ghost' | 'dashed' | 'icon';
|
variant?: 'primary' | 'ghost' | 'dashed' | 'icon' | 'danger' | 'outline' | 'link' | 'secondary' | 'success';
|
||||||
type?: 'button' | 'submit';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
type?: 'button' | 'submit' | 'reset';
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{ variant: 'primary', type: 'button' },
|
{
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
type: 'button',
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
block: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -14,9 +23,12 @@ withDefaults(
|
|||||||
<button
|
<button
|
||||||
:type="type"
|
:type="type"
|
||||||
class="ui-btn"
|
class="ui-btn"
|
||||||
:class="[variant, { block }]"
|
:class="[variant, size, { block, loading }]"
|
||||||
:disabled="disabled"
|
:disabled="disabled || loading"
|
||||||
|
:aria-busy="loading"
|
||||||
>
|
>
|
||||||
|
<span v-if="loading" class="spinner" aria-hidden="true"></span>
|
||||||
|
<slot name="icon" />
|
||||||
<slot />
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -26,18 +38,52 @@ withDefaults(
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.35rem;
|
gap: var(--sp-2);
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h-md);
|
||||||
padding: 0 0.95rem;
|
height: var(--control-h-md);
|
||||||
|
padding: 0 0.85rem;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: #fff;
|
color: var(--accent-contrast);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 700;
|
font-weight: var(--fw-semibold);
|
||||||
font-size: 0.88rem;
|
font-size: var(--fs-sm);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast),
|
||||||
|
box-shadow var(--transition-fast), opacity var(--transition-fast);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sizes */
|
||||||
|
.ui-btn.sm {
|
||||||
|
min-height: var(--control-h-sm);
|
||||||
|
height: var(--control-h-sm);
|
||||||
|
padding: 0 0.65rem;
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.md {
|
||||||
|
min-height: var(--control-h-md);
|
||||||
|
height: var(--control-h-md);
|
||||||
|
padding: 0 0.85rem;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.lg {
|
||||||
|
min-height: var(--control-h-lg);
|
||||||
|
height: var(--control-h-lg);
|
||||||
|
padding: 0 1.15rem;
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn:hover:not(:disabled) {
|
||||||
|
background: var(--accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-btn:disabled {
|
.ui-btn:disabled {
|
||||||
@@ -49,28 +95,125 @@ withDefaults(
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
.ui-btn.ghost {
|
.ui-btn.ghost {
|
||||||
background: #fff;
|
background: transparent;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
border-color: var(--line);
|
border-color: var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-btn.ghost:hover:not(:disabled) {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.outline {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.outline:hover:not(:disabled) {
|
||||||
|
background: var(--accent-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.secondary {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
color: var(--ink-secondary);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.secondary:hover:not(:disabled) {
|
||||||
|
background: var(--line);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.danger:hover:not(:disabled) {
|
||||||
|
background: var(--danger-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.success {
|
||||||
|
background: var(--success);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.success:hover:not(:disabled) {
|
||||||
|
background: var(--success-deep);
|
||||||
|
}
|
||||||
|
|
||||||
.ui-btn.dashed {
|
.ui-btn.dashed {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1.5px dashed currentColor;
|
color: var(--muted);
|
||||||
font-weight: 700;
|
border: 1.5px dashed var(--line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.dashed:hover:not(:disabled) {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.link {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 0 var(--sp-1);
|
||||||
|
min-height: auto;
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.link:hover:not(:disabled) {
|
||||||
|
background: transparent;
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-btn.icon {
|
.ui-btn.icon {
|
||||||
width: var(--control-h);
|
width: var(--control-h-md);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--accent-deep);
|
color: var(--accent-deep);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-btn.icon.sm {
|
||||||
|
width: var(--control-h-sm);
|
||||||
|
min-height: var(--control-h-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.icon.lg {
|
||||||
|
width: var(--control-h-lg);
|
||||||
|
min-height: var(--control-h-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-btn.icon:hover:not(:disabled) {
|
||||||
|
background: var(--accent-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
.ui-btn:focus-visible {
|
.ui-btn:focus-visible {
|
||||||
outline: 2px solid var(--accent-soft);
|
outline: 2px solid var(--accent-soft);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
.spinner {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-right-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: ui-spin 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ui-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,119 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||||
|
shadow?: 'none' | 'xs' | 'sm' | 'md';
|
||||||
|
interactive?: boolean;
|
||||||
|
title?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
padding: 'md',
|
||||||
|
shadow: 'xs',
|
||||||
|
interactive: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="ui-card">
|
<div
|
||||||
<slot />
|
class="ui-card"
|
||||||
|
:class="[
|
||||||
|
`pad-${padding}`,
|
||||||
|
`shadow-${shadow}`,
|
||||||
|
{ interactive }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<header v-if="title || $slots.header" class="card-head">
|
||||||
|
<slot name="header">
|
||||||
|
<h3 v-if="title" class="card-title">{{ title }}</h3>
|
||||||
|
</slot>
|
||||||
|
<div v-if="$slots.actions" class="card-actions">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="card-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<footer v-if="$slots.footer" class="card-foot">
|
||||||
|
<slot name="footer" />
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.ui-card {
|
.ui-card {
|
||||||
background: #fff;
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: var(--pad-modal);
|
box-sizing: border-box;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Padding variants */
|
||||||
|
.ui-card.pad-none {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card.pad-sm {
|
||||||
|
padding: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card.pad-md {
|
||||||
|
padding: var(--sp-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card.pad-lg {
|
||||||
|
padding: var(--sp-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shadow variants */
|
||||||
|
.ui-card.shadow-none {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card.shadow-xs {
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card.shadow-sm {
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card.shadow-md {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card.interactive:hover {
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding-bottom: var(--sp-3);
|
||||||
|
margin-bottom: var(--sp-4);
|
||||||
|
border-bottom: 1px solid var(--line-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-md);
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-foot {
|
||||||
|
padding-top: var(--sp-3);
|
||||||
|
margin-top: var(--sp-4);
|
||||||
|
border-top: 1px solid var(--line-light);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,54 +1,114 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts">
|
||||||
import { use } from 'echarts/core';
|
import { use } from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { LineChart, PieChart } from 'echarts/charts';
|
import { LineChart, PieChart, BarChart } from 'echarts/charts';
|
||||||
import {
|
import {
|
||||||
GridComponent,
|
GridComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
|
TitleComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
import VChart from 'vue-echarts';
|
|
||||||
import type { ComposeOption } from 'echarts/core';
|
import type { ComposeOption } from 'echarts/core';
|
||||||
import type { LineSeriesOption, PieSeriesOption } from 'echarts/charts';
|
import type { LineSeriesOption, PieSeriesOption, BarSeriesOption } from 'echarts/charts';
|
||||||
import type {
|
import type {
|
||||||
GridComponentOption,
|
GridComponentOption,
|
||||||
LegendComponentOption,
|
LegendComponentOption,
|
||||||
TooltipComponentOption,
|
TooltipComponentOption,
|
||||||
|
TitleComponentOption,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
|
|
||||||
use([
|
use([
|
||||||
CanvasRenderer,
|
CanvasRenderer,
|
||||||
LineChart,
|
LineChart,
|
||||||
PieChart,
|
PieChart,
|
||||||
|
BarChart,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
|
TitleComponent,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type UiChartOption = ComposeOption<
|
export type UiChartOption = ComposeOption<
|
||||||
| LineSeriesOption
|
| LineSeriesOption
|
||||||
| PieSeriesOption
|
| PieSeriesOption
|
||||||
|
| BarSeriesOption
|
||||||
| GridComponentOption
|
| GridComponentOption
|
||||||
| TooltipComponentOption
|
| TooltipComponentOption
|
||||||
| LegendComponentOption
|
| LegendComponentOption
|
||||||
|
| TitleComponentOption
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared palette matching Montact CRM design tokens
|
||||||
|
*/
|
||||||
|
export const CHART_PALETTE = [
|
||||||
|
'#14b8a6', // Teal (Accent)
|
||||||
|
'#3b82f6', // Blue
|
||||||
|
'#f59e0b', // Amber
|
||||||
|
'#10b981', // Emerald
|
||||||
|
'#8b5cf6', // Violet
|
||||||
|
'#f43f5e', // Rose
|
||||||
|
'#06b6d4', // Cyan
|
||||||
|
'#eab308', // Yellow
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import VChart from 'vue-echarts';
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
option: UiChartOption;
|
option: UiChartOption;
|
||||||
height?: string;
|
height?: string;
|
||||||
|
loading?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{ height: '220px' },
|
{ height: '260px', loading: false },
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VChart class="ui-chart" :option="option" :style="{ height }" autoresize />
|
<div class="ui-chart-container" :style="{ height }">
|
||||||
|
<div v-if="loading" class="chart-loading">
|
||||||
|
<div class="chart-spinner"></div>
|
||||||
|
</div>
|
||||||
|
<VChart v-else class="ui-chart" :option="option" autoresize />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.ui-chart {
|
.ui-chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-loading {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-spinner {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: chart-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chart-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
|
||||||
|
type IconName = InstanceType<typeof UiIcon>['$props']['name'];
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: IconName;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
title: 'موردی یافت نشد',
|
||||||
|
icon: 'search',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ui-empty-state">
|
||||||
|
<div class="icon-wrap">
|
||||||
|
<slot name="icon">
|
||||||
|
<UiIcon :name="icon" :size="32" />
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<h3 class="title">{{ title }}</h3>
|
||||||
|
<p v-if="description" class="description">{{ description }}</p>
|
||||||
|
<slot />
|
||||||
|
<div v-if="$slots.action" class="action-wrap">
|
||||||
|
<slot name="action" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ui-empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--sp-10) var(--sp-6);
|
||||||
|
gap: var(--sp-3);
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px dashed var(--line-strong);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrap {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: var(--sp-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-md);
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
max-width: 360px;
|
||||||
|
line-height: var(--lh-relaxed);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-wrap {
|
||||||
|
margin-top: var(--sp-2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
label?: string;
|
||||||
|
forId?: string;
|
||||||
|
required?: boolean;
|
||||||
|
hint?: string;
|
||||||
|
error?: string | boolean;
|
||||||
|
horizontal?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
horizontal: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ui-field" :class="{ horizontal, hasError: !!error }">
|
||||||
|
<div v-if="label || $slots.label" class="label-row">
|
||||||
|
<label v-if="label" :for="forId" class="field-label">
|
||||||
|
{{ label }}
|
||||||
|
<span v-if="required" class="required-mark" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<slot v-else name="label" />
|
||||||
|
<slot name="label-extra" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-wrap">
|
||||||
|
<slot />
|
||||||
|
<p v-if="typeof error === 'string' && error" class="field-error" role="alert">
|
||||||
|
{{ error }}
|
||||||
|
</p>
|
||||||
|
<p v-else-if="hint || $slots.hint" class="field-hint">
|
||||||
|
<slot name="hint">{{ hint }}</slot>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ui-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-field.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-field.horizontal .label-row {
|
||||||
|
flex: 0 0 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-field.horizontal .control-wrap {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
color: var(--ink-secondary);
|
||||||
|
line-height: var(--lh-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-mark {
|
||||||
|
color: var(--danger);
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-error {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--danger);
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-hint {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: var(--lh-normal);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,7 +21,7 @@ const open = ref(false);
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<slot name="before" />
|
<slot name="before" />
|
||||||
<UiSearch v-model="search" :placeholder="placeholder" class="search" />
|
<UiSearch v-model="search" :placeholder="placeholder" class="search" />
|
||||||
<UiButton v-if="showFilters" type="button" @click="open = !open">
|
<UiButton v-if="showFilters" variant="secondary" type="button" @click="open = !open">
|
||||||
<UiIcon name="filter" :size="16" />
|
<UiIcon name="filter" :size="16" />
|
||||||
فیلترها
|
فیلترها
|
||||||
</UiButton>
|
</UiButton>
|
||||||
@@ -54,9 +54,10 @@ const open = ref(false);
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
padding: var(--pad-modal);
|
padding: var(--sp-4);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,48 +4,122 @@ withDefaults(
|
|||||||
type?: string;
|
type?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
maxlength?: number;
|
error?: boolean | string;
|
||||||
minlength?: number;
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
maxlength?: number | string;
|
||||||
|
minlength?: number | string;
|
||||||
|
id?: string;
|
||||||
|
autocomplete?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{ type: 'text' },
|
{
|
||||||
|
type: 'text',
|
||||||
|
size: 'md',
|
||||||
|
disabled: false,
|
||||||
|
readonly: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const model = defineModel<string>({ default: '' });
|
const model = defineModel<string | number>({ default: '' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<input
|
<div class="ui-input-wrapper" :class="[size, { disabled, hasError: !!error }]">
|
||||||
v-model="model"
|
<slot name="prefix" />
|
||||||
class="ui-input"
|
<input
|
||||||
:type="type"
|
:id="id"
|
||||||
:placeholder="placeholder"
|
v-model="model"
|
||||||
:disabled="disabled"
|
class="ui-input"
|
||||||
:required="required"
|
:type="type"
|
||||||
:maxlength="maxlength"
|
:placeholder="placeholder"
|
||||||
:minlength="minlength"
|
:disabled="disabled"
|
||||||
/>
|
:readonly="readonly"
|
||||||
|
:required="required"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:minlength="minlength"
|
||||||
|
:autocomplete="autocomplete"
|
||||||
|
:aria-invalid="!!error"
|
||||||
|
/>
|
||||||
|
<slot name="suffix" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.ui-input {
|
.ui-input-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h-md);
|
||||||
padding: 0 0.85rem;
|
padding: 0 var(--sp-3);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: var(--surface);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font: inherit;
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-input:focus {
|
.ui-input-wrapper.sm {
|
||||||
|
min-height: var(--control-h-sm);
|
||||||
|
padding: 0 var(--sp-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input-wrapper.md {
|
||||||
|
min-height: var(--control-h-md);
|
||||||
|
padding: 0 var(--sp-3);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input-wrapper.lg {
|
||||||
|
min-height: var(--control-h-lg);
|
||||||
|
padding: 0 var(--sp-4);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--fs-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input::placeholder {
|
||||||
|
color: var(--muted-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input-wrapper:focus-within {
|
||||||
outline: 2px solid var(--accent-soft);
|
outline: 2px solid var(--accent-soft);
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-input-wrapper.hasError {
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input-wrapper.hasError:focus-within {
|
||||||
|
outline: 2px solid var(--danger-soft);
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input-wrapper.disabled {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-input:disabled {
|
.ui-input:disabled {
|
||||||
opacity: 0.6;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ function onBackdrop(event: MouseEvent) {
|
|||||||
z-index: 80;
|
z-index: 80;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 1.25rem;
|
padding: 16px;
|
||||||
background: rgba(15, 23, 42, 0.35);
|
background: rgba(15, 23, 42, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,55 +72,122 @@ function onBackdrop(event: MouseEvent) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius, 10px);
|
||||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.16);
|
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.16);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.head,
|
|
||||||
.body,
|
|
||||||
.foot {
|
|
||||||
padding: var(--pad-modal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.head {
|
.head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding-bottom: 0;
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.head h2 {
|
.head h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.05rem;
|
font-size: 16px;
|
||||||
font-weight: 800;
|
font-weight: 500;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
width: var(--control-h);
|
width: 36px;
|
||||||
height: var(--control-h);
|
height: 36px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius, 10px);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
color: var(--ink);
|
||||||
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
overflow: auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding-top: 1rem;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body :deep(.crm-page.embedded),
|
||||||
|
.body :deep(.page.embedded),
|
||||||
|
.body :deep(.product-form-page.embedded),
|
||||||
|
.body :deep(.company-form-page.embedded) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body :deep(.page.embedded > .form),
|
||||||
|
.body :deep(.page.embedded > form),
|
||||||
|
.body :deep(.crm-page.embedded > .form),
|
||||||
|
.body :deep(.crm-page.embedded > form),
|
||||||
|
.body :deep(.crm-page.embedded > .crm-form) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body :deep(.embed-actions),
|
||||||
|
.body :deep(.embedded-actions),
|
||||||
|
.body :deep(.form-bottom-actions),
|
||||||
|
.body :deep(.form-footer-actions) {
|
||||||
|
order: 99;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: sticky;
|
||||||
|
bottom: -16px;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-inline: -16px;
|
||||||
|
margin-bottom: -16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 -4px 14px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body :deep(.embed-actions .btn),
|
||||||
|
.body :deep(.embedded-actions .btn),
|
||||||
|
.body :deep(.embed-actions .ui-btn),
|
||||||
|
.body :deep(.embedded-actions .ui-btn),
|
||||||
|
.body :deep(.form-bottom-actions .btn),
|
||||||
|
.body :deep(.form-bottom-actions .ui-btn),
|
||||||
|
.body :deep(.form-footer-actions .btn),
|
||||||
|
.body :deep(.form-footer-actions .ui-btn) {
|
||||||
|
border-radius: var(--radius, 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.foot {
|
.foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 0.5rem;
|
gap: 12px;
|
||||||
padding-top: 0;
|
padding: 16px;
|
||||||
border-top: 1px solid #f1f5f9;
|
border-top: 1px solid var(--line);
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
@@ -138,62 +205,15 @@ function onBackdrop(event: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.head {
|
.head {
|
||||||
flex-shrink: 0;
|
padding: 16px;
|
||||||
padding: 0.75rem 1rem 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
flex: 1;
|
padding: 16px;
|
||||||
min-height: 0;
|
|
||||||
overflow: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body:has(.page.embedded) {
|
|
||||||
overflow: hidden;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body :deep(.page.embedded) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body :deep(.page.embedded > .form),
|
|
||||||
.body :deep(.page.embedded > form) {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body :deep(.embed-actions) {
|
|
||||||
order: 20;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
position: sticky;
|
|
||||||
bottom: 0;
|
|
||||||
margin-top: auto;
|
|
||||||
margin-inline: -1rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom));
|
|
||||||
background: #fff;
|
|
||||||
border-top: 1px solid #f1f5f9;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body :deep(.embed-actions .btn) {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.foot {
|
.foot {
|
||||||
flex-shrink: 0;
|
padding: 16px calc(16px + env(safe-area-inset-bottom));
|
||||||
padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
title?: string;
|
||||||
|
lead?: string;
|
||||||
|
description?: string;
|
||||||
|
backTo?: string | object;
|
||||||
|
}>(),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="ui-page-header">
|
||||||
|
<div class="header-main">
|
||||||
|
<RouterLink v-if="backTo" :to="backTo" class="back-link" title="بازگشت">
|
||||||
|
<UiIcon name="arrow-right" :size="18" />
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<div class="titles">
|
||||||
|
<h1 v-if="title" class="title">{{ title }}</h1>
|
||||||
|
<slot v-else name="title" />
|
||||||
|
|
||||||
|
<p v-if="lead || description" class="lead">{{ lead || description }}</p>
|
||||||
|
<slot v-else name="lead" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.actions" class="actions">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ui-page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: var(--control-h-md);
|
||||||
|
height: var(--control-h-md);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--ink-secondary);
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
color: var(--accent-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titles {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-2xl);
|
||||||
|
font-weight: var(--fw-extrabold);
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: var(--lh-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: var(--lh-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.ui-page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--fs-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions > * {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import UiButton from './UiButton.vue';
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
page: number;
|
||||||
|
totalPages?: number;
|
||||||
|
totalItems?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:page': [page: number];
|
||||||
|
change: [page: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const effectiveTotalPages = computed(() => {
|
||||||
|
if (props.totalPages != null) return Math.max(1, props.totalPages);
|
||||||
|
if (props.totalItems != null && props.pageSize != null && props.pageSize > 0) {
|
||||||
|
return Math.max(1, Math.ceil(props.totalItems / props.pageSize));
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
function goTo(p: number) {
|
||||||
|
if (p < 1 || p > effectiveTotalPages.value || p === props.page) return;
|
||||||
|
emit('update:page', p);
|
||||||
|
emit('change', p);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav v-if="effectiveTotalPages > 1 || totalItems !== undefined" class="ui-pagination" aria-label="صفحهبندی">
|
||||||
|
<div class="pagination-info">
|
||||||
|
<span v-if="totalItems !== undefined">
|
||||||
|
مجموع: <strong>{{ totalItems }}</strong> مورد
|
||||||
|
</span>
|
||||||
|
<span v-if="effectiveTotalPages > 1" class="page-count">
|
||||||
|
(صفحه {{ page }} از {{ effectiveTotalPages }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="effectiveTotalPages > 1" class="pagination-controls">
|
||||||
|
<UiButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
:disabled="page <= 1"
|
||||||
|
title="صفحه اول"
|
||||||
|
@click="goTo(1)"
|
||||||
|
>
|
||||||
|
<UiIcon name="chevrons-right" :size="14" />
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
:disabled="page <= 1"
|
||||||
|
title="صفحه قبل"
|
||||||
|
@click="goTo(page - 1)"
|
||||||
|
>
|
||||||
|
<UiIcon name="chevron-right" :size="14" />
|
||||||
|
قبلی
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<span class="current-indicator">
|
||||||
|
{{ page }} / {{ effectiveTotalPages }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
:disabled="page >= effectiveTotalPages"
|
||||||
|
title="صفحه بعد"
|
||||||
|
@click="goTo(page + 1)"
|
||||||
|
>
|
||||||
|
بعدی
|
||||||
|
<UiIcon name="chevron-left" :size="14" />
|
||||||
|
</UiButton>
|
||||||
|
|
||||||
|
<UiButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
:disabled="page >= effectiveTotalPages"
|
||||||
|
title="صفحه آخر"
|
||||||
|
@click="goTo(effectiveTotalPages)"
|
||||||
|
>
|
||||||
|
<UiIcon name="chevrons-left" :size="14" />
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ui-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding-top: var(--sp-4);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-info {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-count {
|
||||||
|
color: var(--muted-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-controls {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-indicator {
|
||||||
|
padding: 0 var(--sp-2);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
color: var(--ink-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.ui-pagination {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -29,12 +29,15 @@ const model = defineModel<string>({ default: '' });
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
height: var(--control-h);
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
padding: 0 0.75rem;
|
padding: 0 0.75rem;
|
||||||
background: #fff;
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-search input {
|
.ui-search input {
|
||||||
@@ -45,11 +48,14 @@ const model = defineModel<string>({ default: '' });
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-search:focus-within {
|
.ui-search:focus-within {
|
||||||
outline: 2px solid var(--accent-soft);
|
outline: none;
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sr {
|
.sr {
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
|
||||||
defineProps<{
|
withDefaults(
|
||||||
disabled?: boolean;
|
defineProps<{
|
||||||
}>();
|
disabled?: boolean;
|
||||||
|
error?: boolean | string;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
id?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
size: 'md',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const model = defineModel<string>({ default: '' });
|
const model = defineModel<string>({ default: '' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<label class="ui-select">
|
<label class="ui-select" :class="[size, { disabled, hasError: !!error }]">
|
||||||
<select v-model="model" :disabled="disabled">
|
<select :id="id" v-model="model" :disabled="disabled" :required="required" :aria-invalid="!!error">
|
||||||
<slot />
|
<slot />
|
||||||
</select>
|
</select>
|
||||||
<UiIcon class="chev" name="chevron" :size="16" />
|
<UiIcon class="chev" name="chevron" :size="size === 'sm' ? 14 : 16" />
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -22,20 +33,45 @@ const model = defineModel<string>({ default: '' });
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-select select {
|
.ui-select select {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h-md);
|
||||||
padding: 0 2rem 0 0.85rem;
|
padding: 0 2rem 0 var(--sp-3);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: var(--surface);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 600;
|
font-weight: var(--fw-medium);
|
||||||
|
font-size: var(--fs-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select.sm select {
|
||||||
|
min-height: var(--control-h-sm);
|
||||||
|
padding: 0 1.75rem 0 var(--sp-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select.md select {
|
||||||
|
min-height: var(--control-h-md);
|
||||||
|
padding: 0 2rem 0 var(--sp-3);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select.lg select {
|
||||||
|
min-height: var(--control-h-lg);
|
||||||
|
padding: 0 2.25rem 0 var(--sp-4);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: var(--fs-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-select select:focus {
|
.ui-select select:focus {
|
||||||
@@ -43,6 +79,21 @@ const model = defineModel<string>({ default: '' });
|
|||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-select.hasError select {
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select.hasError select:focus {
|
||||||
|
outline: 2px solid var(--danger-soft);
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select.disabled select {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.chev {
|
.chev {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0.65rem;
|
left: 0.65rem;
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
error?: boolean | string;
|
||||||
|
rows?: number;
|
||||||
|
maxlength?: number;
|
||||||
|
id?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
rows: 3,
|
||||||
|
disabled: false,
|
||||||
|
readonly: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const model = defineModel<string>({ default: '' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<textarea
|
||||||
|
:id="id"
|
||||||
|
v-model="model"
|
||||||
|
class="ui-textarea"
|
||||||
|
:class="{ disabled, hasError: !!error }"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="disabled"
|
||||||
|
:readonly="readonly"
|
||||||
|
:required="required"
|
||||||
|
:rows="rows"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:aria-invalid="!!error"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ui-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: var(--sp-3);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--ink);
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
line-height: var(--lh-normal);
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-textarea::placeholder {
|
||||||
|
color: var(--muted-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-textarea:focus {
|
||||||
|
outline: 2px solid var(--accent-soft);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-textarea.hasError {
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-textarea.hasError:focus {
|
||||||
|
outline: 2px solid var(--danger-soft);
|
||||||
|
border-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-textarea.disabled {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export { default as UiButton } from './UiButton.vue';
|
||||||
|
export { default as UiInput } from './UiInput.vue';
|
||||||
|
export { default as UiTextarea } from './UiTextarea.vue';
|
||||||
|
export { default as UiSelect } from './UiSelect.vue';
|
||||||
|
export { default as UiField } from './UiField.vue';
|
||||||
|
export { default as UiCard } from './UiCard.vue';
|
||||||
|
export { default as UiModal } from './UiModal.vue';
|
||||||
|
export { default as UiPageHeader } from './UiPageHeader.vue';
|
||||||
|
export { default as UiBadge } from './UiBadge.vue';
|
||||||
|
export { default as UiPagination } from './UiPagination.vue';
|
||||||
|
export { default as UiEmptyState } from './UiEmptyState.vue';
|
||||||
|
export { default as UiSearch } from './UiSearch.vue';
|
||||||
|
export { default as UiFilterBar } from './UiFilterBar.vue';
|
||||||
|
export { default as UiSegmented } from './UiSegmented.vue';
|
||||||
|
export { default as UiChart, CHART_PALETTE, type UiChartOption } from './UiChart.vue';
|
||||||
@@ -70,12 +70,12 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* map 800 → Black */
|
/* map 800 → Bold (avoid oversized heavy Black in UI) */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "IRANSans";
|
font-family: "IRANSans";
|
||||||
src:
|
src:
|
||||||
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Black.woff2") format("woff2"),
|
url("../assets/fonts/iranSans/woff2/IRANSansWeb(FaNum)_Bold.woff2") format("woff2"),
|
||||||
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Black.woff") format("woff");
|
url("../assets/fonts/iranSans/woff/IRANSansWeb(FaNum)_Bold.woff") format("woff");
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
|
|||||||
@@ -1,20 +1,131 @@
|
|||||||
@import "./fonts.css";
|
@import "./fonts.css";
|
||||||
|
@import "./tokens.css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
/* Surface & Background */
|
||||||
--bg: #f8fafc;
|
--bg: #f8fafc;
|
||||||
--surface: #ffffff;
|
--surface: #ffffff;
|
||||||
|
--surface-subtle: #f1f5f9;
|
||||||
|
--surface-hover: #f8fafc;
|
||||||
|
|
||||||
|
/* Typography & Foreground */
|
||||||
|
--font: "IRANSans", Tahoma, sans-serif;
|
||||||
--ink: #0f172a;
|
--ink: #0f172a;
|
||||||
|
--ink-secondary: #334155;
|
||||||
--muted: #64748b;
|
--muted: #64748b;
|
||||||
|
--muted-light: #94a3b8;
|
||||||
--line: #e2e8f0;
|
--line: #e2e8f0;
|
||||||
|
--line-light: #f1f5f9;
|
||||||
|
--line-strong: #cbd5e1;
|
||||||
|
|
||||||
|
/* Accent / Primary (Teal) */
|
||||||
--accent: #14b8a6;
|
--accent: #14b8a6;
|
||||||
|
--accent-hover: #0d9488;
|
||||||
--accent-deep: #0f766e;
|
--accent-deep: #0f766e;
|
||||||
--accent-soft: #ccfbf1;
|
--accent-soft: #ccfbf1;
|
||||||
|
--accent-subtle: #f0fdfa;
|
||||||
|
--accent-contrast: #ffffff;
|
||||||
|
|
||||||
|
/* Semantics */
|
||||||
--danger: #b42318;
|
--danger: #b42318;
|
||||||
--shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
--danger-hover: #991b1b;
|
||||||
|
--danger-soft: #fee2e2;
|
||||||
|
--danger-deep: #7f1d1d;
|
||||||
|
|
||||||
|
--success: #10b981;
|
||||||
|
--success-soft: #d1fae5;
|
||||||
|
--success-deep: #047857;
|
||||||
|
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--warning-soft: #fef3c7;
|
||||||
|
--warning-deep: #b45309;
|
||||||
|
|
||||||
|
--info: #0284c7;
|
||||||
|
--info-soft: #e0f2fe;
|
||||||
|
--info-deep: #0369a1;
|
||||||
|
|
||||||
|
/* Typography Scale */
|
||||||
|
--fs-xs: 0.75rem; /* 12px */
|
||||||
|
--fs-sm: 0.8125rem; /* 13px */
|
||||||
|
--fs-base: 0.875rem; /* 14px */
|
||||||
|
--fs-md: 0.9375rem; /* 15px */
|
||||||
|
--fs-lg: 1.0625rem; /* 17px */
|
||||||
|
--fs-xl: 1.25rem; /* 20px */
|
||||||
|
--fs-2xl: 1.5rem; /* 24px */
|
||||||
|
--fs-3xl: 1.75rem; /* 28px */
|
||||||
|
|
||||||
|
--fw-normal: 400;
|
||||||
|
--fw-medium: 500;
|
||||||
|
--fw-semibold: 600;
|
||||||
|
--fw-bold: 700;
|
||||||
|
--fw-extrabold: 700;
|
||||||
|
|
||||||
|
--lh-tight: 1.25;
|
||||||
|
--lh-normal: 1.5;
|
||||||
|
--lh-relaxed: 1.625;
|
||||||
|
|
||||||
|
/* Spacing Scale */
|
||||||
|
--sp-1: 0.25rem; /* 4px */
|
||||||
|
--sp-2: 0.5rem; /* 8px */
|
||||||
|
--sp-3: 0.75rem; /* 12px */
|
||||||
|
--sp-4: 1rem; /* 16px */
|
||||||
|
--sp-5: 1.25rem; /* 20px */
|
||||||
|
--sp-6: 1.5rem; /* 24px */
|
||||||
|
--sp-8: 2rem; /* 32px */
|
||||||
|
--sp-10: 2.5rem; /* 40px */
|
||||||
|
|
||||||
|
/* Radius - Single Source of Truth (Globally 10px as requested) */
|
||||||
--radius: 10px;
|
--radius: 10px;
|
||||||
|
--radius-xs: var(--radius);
|
||||||
|
--radius-sm: var(--radius);
|
||||||
|
--radius-md: var(--radius);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: var(--radius);
|
||||||
|
--radius-2xl: var(--radius);
|
||||||
|
--radius-full: 9999px;
|
||||||
|
|
||||||
|
/* Control Dimensions */
|
||||||
|
--control-h-sm: 30px;
|
||||||
--control-h: 36px;
|
--control-h: 36px;
|
||||||
--pad-modal: 24px;
|
--control-h-md: 36px;
|
||||||
--font: "IRANSans", Tahoma, sans-serif;
|
--control-h-lg: 42px;
|
||||||
|
--pad-modal: 16px;
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
--shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.05);
|
||||||
|
--shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.08), 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||||
|
--shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08);
|
||||||
|
--shadow-lg: 0 12px 32px rgba(15, 23, 42, 0.12);
|
||||||
|
--shadow-xl: 0 20px 48px rgba(15, 23, 42, 0.16);
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
--transition-fast: 150ms ease;
|
||||||
|
--transition-normal: 200ms ease;
|
||||||
|
|
||||||
|
/* Z-index */
|
||||||
|
--z-dropdown: 50;
|
||||||
|
--z-sticky: 60;
|
||||||
|
--z-modal-backdrop: 80;
|
||||||
|
--z-modal: 90;
|
||||||
|
--z-toast: 100;
|
||||||
|
/* Compatibility aliases for legacy views */
|
||||||
|
--color-border: var(--line);
|
||||||
|
--color-border-light: var(--line-light);
|
||||||
|
--color-text: var(--ink);
|
||||||
|
--color-text-primary: var(--ink);
|
||||||
|
--color-text-secondary: var(--ink-secondary);
|
||||||
|
--color-text-muted: var(--muted);
|
||||||
|
--color-text-emphasis: var(--ink);
|
||||||
|
--color-surface: var(--surface);
|
||||||
|
--color-surface-sunken: var(--surface-subtle);
|
||||||
|
--color-primary: var(--accent);
|
||||||
|
--color-primary-dark: var(--accent-deep);
|
||||||
|
--color-primary-light: var(--accent-soft);
|
||||||
|
--color-primary-surface: var(--accent-subtle);
|
||||||
|
--color-danger: var(--danger);
|
||||||
|
--color-danger-surface: var(--danger-soft);
|
||||||
|
--color-warning: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -64,16 +175,20 @@ textarea {
|
|||||||
.btn {
|
.btn {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
height: var(--control-h);
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
padding: 0 1rem;
|
padding: 0 0.85rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast), opacity var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
|
|||||||
@@ -0,0 +1,496 @@
|
|||||||
|
/**
|
||||||
|
* Montact CRM (منتکت) Design System - Core Tokens & Utilities
|
||||||
|
* RTL-first, IRANSans typography, unified spacing, controls & surfaces.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Page Layout
|
||||||
|
========================================================================== */
|
||||||
|
.crm-page,
|
||||||
|
.page,
|
||||||
|
.dashboard,
|
||||||
|
.profile,
|
||||||
|
.notes-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-5);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-page.narrow,
|
||||||
|
.crm-page.wide,
|
||||||
|
.crm-page.full {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-page.embedded {
|
||||||
|
padding: 0;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Page Header
|
||||||
|
========================================================================== */
|
||||||
|
.crm-page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-page-header-titles {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-xl);
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: var(--lh-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-page-lead {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: var(--lh-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Toolbar & Filter Bar
|
||||||
|
========================================================================== */
|
||||||
|
.crm-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-toolbar-stretch,
|
||||||
|
.crm-toolbar .toolbar-search,
|
||||||
|
.crm-toolbar .search-box,
|
||||||
|
.crm-toolbar .crm-search {
|
||||||
|
flex: 1 1 200px;
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-toolbar .toolbar-select,
|
||||||
|
.crm-toolbar .ui-select:not(.block) {
|
||||||
|
width: auto;
|
||||||
|
min-width: 130px;
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-toolbar .ui-btn,
|
||||||
|
.crm-toolbar .btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Card & Container Surfaces
|
||||||
|
========================================================================== */
|
||||||
|
.crm-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
|
padding: var(--sp-5);
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: box-shadow var(--transition-fast), border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-card-sm {
|
||||||
|
padding: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-card-lg {
|
||||||
|
padding: var(--sp-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-card-interactive:hover {
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding-bottom: var(--sp-3);
|
||||||
|
margin-bottom: var(--sp-4);
|
||||||
|
border-bottom: 1px solid var(--line-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-card-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-md);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-card-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Forms & Field Layouts
|
||||||
|
========================================================================== */
|
||||||
|
.crm-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-5);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-form-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-form-section-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-md);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
color: var(--ink);
|
||||||
|
padding-bottom: var(--sp-2);
|
||||||
|
border-bottom: 1px solid var(--line-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: var(--sp-4);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-form-grid-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--sp-4);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-form-grid-3 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: var(--sp-4);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-form-grid-4 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: var(--sp-4);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-col-span-2 {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-col-span-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
color: var(--ink-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-required-indicator {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-hint {
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: var(--lh-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-field-error {
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--danger);
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-form-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding-top: var(--sp-4);
|
||||||
|
margin-top: var(--sp-2);
|
||||||
|
border-top: 1px solid var(--line-light);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Data Table
|
||||||
|
========================================================================== */
|
||||||
|
.crm-table-container {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: right;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-table th {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
padding: var(--sp-3) var(--sp-4);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-table td {
|
||||||
|
padding: var(--sp-3) var(--sp-4);
|
||||||
|
border-bottom: 1px solid var(--line-light);
|
||||||
|
color: var(--ink);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-table tbody tr:hover td {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-table-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Badges & Status Indicators
|
||||||
|
========================================================================== */
|
||||||
|
.crm-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-badge-sm {
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-badge-neutral {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
color: var(--ink-secondary);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-badge-accent,
|
||||||
|
.crm-badge-primary {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-deep);
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-badge-success {
|
||||||
|
background: var(--success-soft);
|
||||||
|
color: var(--success-deep);
|
||||||
|
border-color: color-mix(in srgb, var(--success) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-badge-warning {
|
||||||
|
background: var(--warning-soft);
|
||||||
|
color: var(--warning-deep);
|
||||||
|
border-color: color-mix(in srgb, var(--warning) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-badge-danger {
|
||||||
|
background: var(--danger-soft);
|
||||||
|
color: var(--danger);
|
||||||
|
border-color: color-mix(in srgb, var(--danger) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-badge-info {
|
||||||
|
background: var(--info-soft);
|
||||||
|
color: var(--info-deep);
|
||||||
|
border-color: color-mix(in srgb, var(--info) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Pagination
|
||||||
|
========================================================================== */
|
||||||
|
.crm-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding-top: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-pagination-info {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-pagination-nav {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Empty States
|
||||||
|
========================================================================== */
|
||||||
|
.crm-empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--sp-10) var(--sp-6);
|
||||||
|
gap: var(--sp-3);
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px dashed var(--line-strong);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-empty-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-md);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-empty-desc {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
max-width: 360px;
|
||||||
|
line-height: var(--lh-relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Typography Utilities
|
||||||
|
========================================================================== */
|
||||||
|
.crm-text-xs { font-size: var(--fs-xs); }
|
||||||
|
.crm-text-sm { font-size: var(--fs-sm); }
|
||||||
|
.crm-text-base { font-size: var(--fs-base); }
|
||||||
|
.crm-text-md { font-size: var(--fs-md); }
|
||||||
|
.crm-text-lg { font-size: var(--fs-lg); }
|
||||||
|
.crm-text-xl { font-size: var(--fs-xl); }
|
||||||
|
.crm-text-2xl { font-size: var(--fs-2xl); }
|
||||||
|
|
||||||
|
.crm-font-normal { font-weight: var(--fw-normal); }
|
||||||
|
.crm-font-medium { font-weight: var(--fw-medium); }
|
||||||
|
.crm-font-semibold { font-weight: var(--fw-semibold); }
|
||||||
|
.crm-font-bold { font-weight: var(--fw-bold); }
|
||||||
|
.crm-font-extrabold { font-weight: var(--fw-extrabold); }
|
||||||
|
|
||||||
|
.crm-text-ink { color: var(--ink); }
|
||||||
|
.crm-text-secondary { color: var(--ink-secondary); }
|
||||||
|
.crm-text-muted { color: var(--muted); }
|
||||||
|
.crm-text-accent { color: var(--accent); }
|
||||||
|
.crm-text-danger { color: var(--danger); }
|
||||||
|
.crm-text-success { color: var(--success); }
|
||||||
|
.crm-text-warning { color: var(--warning); }
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Responsive Breakpoints
|
||||||
|
========================================================================== */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.crm-page {
|
||||||
|
padding: 0;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-page-title {
|
||||||
|
font-size: var(--fs-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-header-actions {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-header-actions > * {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-form-grid-2,
|
||||||
|
.crm-form-grid-3,
|
||||||
|
.crm-form-grid-4 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-col-span-2 {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-pagination {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -405,7 +405,7 @@ onMounted(() => {
|
|||||||
h1 {
|
h1 {
|
||||||
margin: 0 0 0.3rem;
|
margin: 0 0 0.3rem;
|
||||||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@@ -461,7 +461,7 @@ h2 {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
padding: 0.75rem 0.95rem;
|
padding: 0.75rem 0.95rem;
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
@@ -493,7 +493,7 @@ h2 {
|
|||||||
|
|
||||||
.calendar {
|
.calendar {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -519,7 +519,7 @@ h2 {
|
|||||||
.day {
|
.day {
|
||||||
min-height: 92px;
|
min-height: 92px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
padding: 0.45rem;
|
padding: 0.45rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -553,7 +553,7 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.day.has .num {
|
.day.has .num {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +594,7 @@ h2 {
|
|||||||
|
|
||||||
.day-panel {
|
.day-panel {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -623,7 +623,7 @@ h2 {
|
|||||||
gap: 0.15rem;
|
gap: 0.15rem;
|
||||||
padding: 0.75rem 0.85rem;
|
padding: 0.75rem 0.85rem;
|
||||||
border: 1px solid #f1f5f9;
|
border: 1px solid #f1f5f9;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ import {
|
|||||||
startOfLocalDay,
|
startOfLocalDay,
|
||||||
toFaDigits,
|
toFaDigits,
|
||||||
} from '@/lib/jalali';
|
} from '@/lib/jalali';
|
||||||
import UiButton from '@/components/ui/UiButton.vue';
|
import {
|
||||||
import UiModal from '@/components/ui/UiModal.vue';
|
UiButton,
|
||||||
|
UiModal,
|
||||||
|
UiPageHeader,
|
||||||
|
UiPagination,
|
||||||
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import ActivityFormView from '@/views/ActivityFormView.vue';
|
import ActivityFormView from '@/views/ActivityFormView.vue';
|
||||||
import type {
|
import type {
|
||||||
@@ -124,8 +128,6 @@ const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE))
|
|||||||
const filterLabel = computed(() => TABS.find((t) => t.key === filter.value)?.label ?? 'امروز');
|
const filterLabel = computed(() => TABS.find((t) => t.key === filter.value)?.label ?? 'امروز');
|
||||||
const trulyEmpty = computed(() => !loading.value && counts.value.all === 0 && !q.value.trim());
|
const trulyEmpty = computed(() => !loading.value && counts.value.all === 0 && !q.value.trim());
|
||||||
const listEmpty = computed(() => !loading.value && !items.value.length);
|
const listEmpty = computed(() => !loading.value && !items.value.length);
|
||||||
const rangeFrom = computed(() => (total.value ? (page.value - 1) * PAGE_SIZE + 1 : 0));
|
|
||||||
const rangeTo = computed(() => Math.min(page.value * PAGE_SIZE, total.value));
|
|
||||||
|
|
||||||
function isLastItem(id: string) {
|
function isLastItem(id: string) {
|
||||||
const rows = items.value;
|
const rows = items.value;
|
||||||
@@ -346,21 +348,19 @@ onMounted(() => {
|
|||||||
<span>فعالیتها</span>
|
<span>فعالیتها</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div>
|
title="فعالیتها"
|
||||||
<p class="eyebrow">پیگیری</p>
|
lead="تماس، جلسه و وظیفه — امروز و عقبافتادهها یکجا."
|
||||||
<h1>فعالیتها</h1>
|
>
|
||||||
<p class="lead">تماس، جلسه و وظیفه — امروز و عقبافتادهها یکجا.</p>
|
<template #actions>
|
||||||
</div>
|
<UiButton variant="ghost" type="button" :disabled="busy" @click.stop="exportExcel">
|
||||||
<div class="head-actions">
|
|
||||||
<button class="btn ghost excel-btn" type="button" :disabled="busy" @click.stop="exportExcel">
|
|
||||||
<span class="btn-full">خروجی اکسل</span>
|
<span class="btn-full">خروجی اکسل</span>
|
||||||
<span class="btn-short">خروجی</span>
|
<span class="btn-short">خروجی</span>
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn ghost excel-btn" type="button" :disabled="busy" @click.stop="pickImportFile">
|
<UiButton variant="ghost" type="button" :disabled="busy" @click.stop="pickImportFile">
|
||||||
<span class="btn-full">ورود از اکسل</span>
|
<span class="btn-full">ورود از اکسل</span>
|
||||||
<span class="btn-short">ورود</span>
|
<span class="btn-short">ورود</span>
|
||||||
</button>
|
</UiButton>
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -368,16 +368,18 @@ onMounted(() => {
|
|||||||
hidden
|
hidden
|
||||||
@change="onImportFile"
|
@change="onImportFile"
|
||||||
/>
|
/>
|
||||||
<UiButton type="button" @click="activityModal = {}">
|
<UiButton variant="primary" @click="activityModal = {}">
|
||||||
<UiIcon name="plus" :size="16" />
|
<UiIcon name="plus" :size="16" />
|
||||||
فعالیت جدید
|
فعالیت جدید
|
||||||
</UiButton>
|
</UiButton>
|
||||||
<RouterLink class="btn ghost" :to="{ name: 'activities-calendar' }">
|
<RouterLink :to="{ name: 'activities-calendar' }">
|
||||||
<UiIcon name="calendar" :size="18" />
|
<UiButton variant="ghost">
|
||||||
تقویم
|
<UiIcon name="calendar" :size="16" />
|
||||||
|
تقویم
|
||||||
|
</UiButton>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</template>
|
||||||
</header>
|
</UiPageHeader>
|
||||||
|
|
||||||
<div class="tabs" role="tablist" aria-label="فیلتر فعالیت">
|
<div class="tabs" role="tablist" aria-label="فیلتر فعالیت">
|
||||||
<button
|
<button
|
||||||
@@ -621,23 +623,13 @@ onMounted(() => {
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="total" class="pager">
|
<UiPagination
|
||||||
<p>نمایش {{ toFaDigits(rangeFrom) }} تا {{ toFaDigits(rangeTo) }} از {{ toFaDigits(total) }} فعالیت</p>
|
v-if="total"
|
||||||
<div class="pages">
|
:page="page"
|
||||||
<button type="button" :disabled="page <= 1" @click="setPage(page - 1)">‹</button>
|
:total-pages="pageCount"
|
||||||
<button
|
:total-items="total"
|
||||||
v-for="n in pageCount"
|
@update:page="setPage"
|
||||||
:key="n"
|
/>
|
||||||
type="button"
|
|
||||||
:class="{ on: n === page }"
|
|
||||||
@click="setPage(n)"
|
|
||||||
>
|
|
||||||
{{ toFaDigits(n) }}
|
|
||||||
</button>
|
|
||||||
<button type="button" :disabled="page >= pageCount" @click="setPage(page + 1)">›</button>
|
|
||||||
</div>
|
|
||||||
<p class="per">نمایش {{ toFaDigits(PAGE_SIZE) }} در هر صفحه</p>
|
|
||||||
</footer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UiModal
|
<UiModal
|
||||||
@@ -737,26 +729,34 @@ h1 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
|
height: var(--control-h);
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
padding: 0 0.95rem;
|
padding: 0 0.85rem;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.btn.primary {
|
||||||
background: #14b8a6;
|
background: var(--accent);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.ghost {
|
.btn.ghost {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #0f766e;
|
color: var(--ink-secondary);
|
||||||
border-color: #99f6e4;
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.ghost:hover {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.wide {
|
.btn.wide {
|
||||||
@@ -802,25 +802,38 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
flex: 1;
|
flex: 1 1 200px;
|
||||||
min-width: 0;
|
min-width: 180px;
|
||||||
|
max-width: 400px;
|
||||||
|
height: var(--control-h);
|
||||||
|
min-height: var(--control-h);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0 0.85rem;
|
padding: 0 0.75rem;
|
||||||
color: #94a3b8;
|
color: var(--muted);
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search:focus-within {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search input {
|
.search input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
outline: none;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0.75rem 0;
|
padding: 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-wrap,
|
.filter-wrap,
|
||||||
@@ -839,7 +852,7 @@ h1 {
|
|||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
min-width: 11rem;
|
min-width: 11rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -859,7 +872,7 @@ h1 {
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0.6rem 0.7rem;
|
padding: 0.6rem 0.7rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
@@ -874,7 +887,7 @@ h1 {
|
|||||||
|
|
||||||
.empty-card {
|
.empty-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 22px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
|
||||||
padding: 2.4rem 1.2rem 2rem;
|
padding: 2.4rem 1.2rem 2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -914,7 +927,7 @@ h1 {
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.65rem 0.8rem;
|
padding: 0.65rem 0.8rem;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -933,7 +946,7 @@ h1 {
|
|||||||
|
|
||||||
.table {
|
.table {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
@@ -950,9 +963,9 @@ h1 {
|
|||||||
.thead {
|
.thead {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
border-bottom: 1px solid #f1f5f9;
|
border-bottom: 1px solid #f1f5f9;
|
||||||
border-radius: 18px 18px 0 0;
|
border-radius: var(--radius) var(--radius) 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trow {
|
.trow {
|
||||||
@@ -962,7 +975,7 @@ h1 {
|
|||||||
|
|
||||||
.trow:last-child {
|
.trow:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-radius: 0 0 18px 18px;
|
border-radius: 0 0 var(--radius) var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.trow.menu-open {
|
.trow.menu-open {
|
||||||
@@ -982,7 +995,7 @@ h1 {
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1100,7 +1113,7 @@ time.note {
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1114,7 +1127,7 @@ time.note {
|
|||||||
.card {
|
.card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #eef2f6;
|
border: 1px solid #eef2f6;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
padding: 0.9rem 0.95rem 0.75rem;
|
padding: 0.9rem 0.95rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1132,7 +1145,7 @@ time.note {
|
|||||||
.card-main time {
|
.card-main time {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: none;
|
background: none;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
}
|
}
|
||||||
@@ -1224,7 +1237,7 @@ time.note {
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,13 +313,12 @@ onMounted(load);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
max-width: 640px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0 auto;
|
margin: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.9rem;
|
gap: 0.9rem;
|
||||||
padding-bottom: 5.8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
@@ -331,7 +330,7 @@ onMounted(load);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
letter-spacing: -0.04em;
|
letter-spacing: -0.04em;
|
||||||
}
|
}
|
||||||
@@ -341,7 +340,7 @@ onMounted(load);
|
|||||||
height: 38px;
|
height: 38px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
@@ -362,7 +361,7 @@ onMounted(load);
|
|||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -377,7 +376,7 @@ onMounted(load);
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0.62rem 0.75rem;
|
padding: 0.62rem 0.75rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
@@ -407,7 +406,7 @@ h1 {
|
|||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 20px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
||||||
padding: 1rem 1.05rem;
|
padding: 1rem 1.05rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ import { appendSpokenText } from '@/lib/spoken-text';
|
|||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import { brand } from '@mento/shared';
|
import { brand } from '@mento/shared';
|
||||||
import UserAvatar from '@/components/UserAvatar.vue';
|
import UserAvatar from '@/components/UserAvatar.vue';
|
||||||
|
import {
|
||||||
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
UiTextarea,
|
||||||
|
} from '@/components/ui';
|
||||||
import type {
|
import type {
|
||||||
ActivityDto,
|
ActivityDto,
|
||||||
ActivityPriority,
|
ActivityPriority,
|
||||||
@@ -338,44 +345,44 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" :class="{ embedded }" @click="typeOpen = assigneeOpen = personOpen = false">
|
<section class="crm-page" :class="{ embedded }" @click="typeOpen = assigneeOpen = personOpen = false">
|
||||||
<div v-if="embedded" class="embed-actions">
|
<UiPageHeader
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="submit('detail')">
|
v-if="!embedded"
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
|
:title="pageTitle"
|
||||||
</button>
|
:back-to="{ name: 'activities' }"
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
|
>
|
||||||
</div>
|
<template #lead>
|
||||||
<header v-if="!embedded" class="desktop-head">
|
<span class="crumb">
|
||||||
<p class="crumb">
|
<RouterLink :to="{ name: 'activities' }">فعالیتها</RouterLink>
|
||||||
<RouterLink :to="{ name: 'activities' }">فعالیتها</RouterLink>
|
<span>›</span>
|
||||||
<span>‹</span>
|
<strong>{{ pageTitle }}</strong>
|
||||||
<strong>{{ pageTitle }}</strong>
|
</span>
|
||||||
</p>
|
</template>
|
||||||
<div class="desk-actions">
|
<template #actions>
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="submit('detail')">
|
<UiButton
|
||||||
<UiIcon name="check-plain" :size="16" />
|
variant="primary"
|
||||||
|
type="button"
|
||||||
|
:disabled="saving"
|
||||||
|
:loading="saving"
|
||||||
|
@click="submit('detail')"
|
||||||
|
>
|
||||||
|
<UiIcon name="check" :size="16" />
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn outline" type="button" :disabled="saving" @click="submit('new')">
|
<UiButton
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="submit('new')"
|
||||||
|
>
|
||||||
<UiIcon name="plus" :size="16" />
|
<UiIcon name="plus" :size="16" />
|
||||||
ذخیره و ایجاد جدید
|
ذخیره و ایجاد جدید
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">
|
<UiButton variant="ghost" type="button" :disabled="saving" @click="cancel">
|
||||||
<UiIcon name="x" :size="16" />
|
|
||||||
انصراف
|
انصراف
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</template>
|
||||||
</header>
|
</UiPageHeader>
|
||||||
|
|
||||||
<header v-if="!embedded" class="mobile-head">
|
|
||||||
<button class="icon-btn" type="button" aria-label="ذخیره" :disabled="saving" @click="submit('detail')">
|
|
||||||
<UiIcon name="check-plain" :size="22" />
|
|
||||||
</button>
|
|
||||||
<h1>{{ pageTitle }}</h1>
|
|
||||||
<button class="icon-btn" type="button" aria-label="بستن" @click="cancel">
|
|
||||||
<UiIcon name="x" :size="20" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<button type="button" :class="{ on: tab === 'main' }" @click="tab = 'main'">اطلاعات اصلی</button>
|
<button type="button" :class="{ on: tab === 'main' }" @click="tab = 'main'">اطلاعات اصلی</button>
|
||||||
@@ -410,7 +417,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="title">عنوان فعالیت <i>*</i></label>
|
<label for="title">عنوان فعالیت <i>*</i></label>
|
||||||
<input
|
<UiInput
|
||||||
id="title"
|
id="title"
|
||||||
v-model="title"
|
v-model="title"
|
||||||
required
|
required
|
||||||
@@ -560,10 +567,10 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
@transcript="(text) => (description = appendSpokenText(description, text))"
|
@transcript="(text) => (description = appendSpokenText(description, text))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<UiTextarea
|
||||||
id="description"
|
id="description"
|
||||||
v-model="description"
|
v-model="description"
|
||||||
rows="8"
|
:rows="8"
|
||||||
placeholder="جزئیات تماس، نتیجه جلسه یا یادداشت مرتبط را بنویسید…"
|
placeholder="جزئیات تماس، نتیجه جلسه یا یادداشت مرتبط را بنویسید…"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -603,114 +610,108 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>وضعیت انجام</dt>
|
<dt>وضعیت انجام</dt>
|
||||||
<dd>{{ statusText }}</dd>
|
<dd>
|
||||||
|
<UiBadge :variant="isDone ? 'success' : 'neutral'" size="sm">
|
||||||
|
{{ statusText }}
|
||||||
|
</UiBadge>
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>اولویت</dt>
|
<dt>اولویت</dt>
|
||||||
<dd>{{ priorityText }}</dd>
|
<dd>
|
||||||
|
<UiBadge
|
||||||
|
:variant="priority === 'very_high' || priority === 'high' ? 'danger' : priority === 'medium' ? 'warning' : 'neutral'"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ priorityText }}
|
||||||
|
</UiBadge>
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="!embedded" class="mobile-save">
|
<footer v-if="!embedded" class="mobile-save">
|
||||||
<button class="btn primary" type="button" :disabled="saving || loading" @click="submit('detail')">
|
<UiButton
|
||||||
|
variant="primary"
|
||||||
|
type="button"
|
||||||
|
:disabled="saving || loading"
|
||||||
|
:loading="saving"
|
||||||
|
@click="submit('detail')"
|
||||||
|
>
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
|
||||||
</button>
|
</UiButton>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Embedded Modal Actions (Fixed at bottom) -->
|
||||||
|
<div v-if="embedded" class="embed-actions">
|
||||||
|
<UiButton
|
||||||
|
variant="primary"
|
||||||
|
type="button"
|
||||||
|
:disabled="saving"
|
||||||
|
:loading="saving"
|
||||||
|
@click="submit('detail')"
|
||||||
|
>
|
||||||
|
<UiIcon name="check" :size="16" />
|
||||||
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره فعالیت' }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="ghost" type="button" :disabled="saving" @click="cancel">انصراف</UiButton>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.crm-page.embedded {
|
||||||
display: grid;
|
|
||||||
gap: 0.85rem;
|
|
||||||
padding-bottom: 5.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page.embedded {
|
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.embed-actions {
|
.embed-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: var(--sp-2);
|
||||||
}
|
margin-bottom: var(--sp-3);
|
||||||
|
|
||||||
.desktop-head {
|
|
||||||
display: none;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumb {
|
.crumb {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: var(--sp-2);
|
||||||
color: #64748b;
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumb a {
|
.crumb a {
|
||||||
color: #0f766e;
|
color: var(--color-primary-dark);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
text-decoration: none;
|
||||||
|
|
||||||
.desk-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-head {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 40px 1fr 40px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-head h1 {
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
color: #0f766e;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.2rem;
|
gap: var(--sp-4);
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs button {
|
.tabs button {
|
||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0.55rem 0;
|
padding: var(--sp-2) 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: #94a3b8;
|
color: var(--color-text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs .on {
|
.tabs .on {
|
||||||
color: #0f766e;
|
color: var(--color-primary);
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
box-shadow: inset 0 -2px 0 #14b8a6;
|
box-shadow: inset 0 -2px 0 var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
@@ -720,32 +721,34 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
.form,
|
.form,
|
||||||
.stack {
|
.stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #fff;
|
background: var(--color-surface);
|
||||||
border-radius: 18px;
|
border: 1px solid var(--color-border);
|
||||||
padding: 0.95rem;
|
border-radius: var(--radius-xl);
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
padding: var(--sp-4);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.4rem;
|
gap: var(--sp-1);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field label,
|
.field label,
|
||||||
.pri-label {
|
.pri-label {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #64748b;
|
color: var(--color-text-muted);
|
||||||
font-size: 0.82rem;
|
font-size: var(--fs-xs);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field i {
|
.field i {
|
||||||
color: #dc2626;
|
color: var(--color-danger);
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,7 +833,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
}
|
}
|
||||||
|
|
||||||
.who b {
|
.who b {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.who small {
|
.who small {
|
||||||
@@ -847,7 +850,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
||||||
max-height: 16rem;
|
max-height: 16rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -861,7 +864,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0.55rem 0.5rem;
|
padding: 0.55rem 0.5rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -937,7 +940,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
background: #f0fdfa;
|
background: #f0fdfa;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1005,7 +1008,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pri.on span {
|
.pri.on span {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reminder :deep(.trigger) {
|
.reminder :deep(.trigger) {
|
||||||
@@ -1026,7 +1029,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
.channel-title {
|
.channel-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1036,7 +1039,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.55rem 0.65rem;
|
padding: 0.55rem 0.65rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
@@ -1109,7 +1112,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 0 0.95rem;
|
padding: 0 0.95rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1176,7 +1179,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0.8rem;
|
top: 0.8rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 20px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem 1.05rem 0.7rem;
|
padding: 1rem 1.05rem 0.7rem;
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
@@ -1195,7 +1198,7 @@ watch([id, isNew], () => void load(), { immediate: true });
|
|||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
padding: 1rem 0.6rem;
|
padding: 1rem 0.6rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
|
import {
|
||||||
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import AutomationCanvas from '@/components/automations/AutomationCanvas.vue';
|
import AutomationCanvas from '@/components/automations/AutomationCanvas.vue';
|
||||||
import type { AutomationAddPayload } from '@/components/automations/AutomationCanvas.vue';
|
import type { AutomationAddPayload } from '@/components/automations/AutomationCanvas.vue';
|
||||||
@@ -138,39 +144,46 @@ function confirmLeave() {
|
|||||||
function startNew() {
|
function startNew() {
|
||||||
if (!confirmLeave()) return;
|
if (!confirmLeave()) return;
|
||||||
hydrate(null);
|
hydrate(null);
|
||||||
ok.value = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRule(row: AutomationRuleDto) {
|
function openRule(row: AutomationRuleDto) {
|
||||||
if (editingId.value === row.id) return;
|
if (editingId.value === row.id) return;
|
||||||
if (!confirmLeave()) return;
|
if (!confirmLeave()) return;
|
||||||
hydrate(row);
|
hydrate(row);
|
||||||
ok.value = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addStep(payload: AutomationAddPayload, index: number) {
|
function addStep(payload: AutomationAddPayload, index: number) {
|
||||||
if (steps.value.length >= MAX_AUTOMATION_STEPS) return;
|
if (steps.value.length >= MAX_AUTOMATION_STEPS) {
|
||||||
const step = payload.kind === 'condition' ? makeCondition() : makeStep(payload.action);
|
error.value = `حداکثر ${toFaDigits(MAX_AUTOMATION_STEPS)} مرحله در هر قانون مجاز است.`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error.value = '';
|
||||||
|
const step =
|
||||||
|
payload.kind === 'condition'
|
||||||
|
? makeCondition()
|
||||||
|
: makeStep(payload.action);
|
||||||
steps.value.splice(index, 0, step);
|
steps.value.splice(index, 0, step);
|
||||||
selected.value = step.id;
|
selected.value = step.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeStep(id: string) {
|
function removeStep(stepId: string) {
|
||||||
const target = steps.value.find((s) => s.id === id);
|
if (steps.value.length <= 1) {
|
||||||
if (!target) return;
|
error.value = 'حداقل یک مرحله باید در زنجیره باقی بماند.';
|
||||||
const actionCount = steps.value.filter((s) => !isAutomationCondition(s)).length;
|
return;
|
||||||
if (!isAutomationCondition(target) && actionCount <= 1) return;
|
}
|
||||||
if (steps.value.length <= 1) return;
|
const i = steps.value.findIndex((s) => s.id === stepId);
|
||||||
steps.value = steps.value.filter((s) => s.id !== id);
|
if (i < 0) return;
|
||||||
if (selected.value === id) selected.value = 'trigger';
|
steps.value.splice(i, 1);
|
||||||
|
if (selected.value === stepId) {
|
||||||
|
const next = steps.value[Math.max(0, i - 1)];
|
||||||
|
selected.value = next?.id ?? 'trigger';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function chainLabel(row: AutomationRuleDto) {
|
function chainLabel(row: AutomationRuleDto) {
|
||||||
const chain = normalizeAutomationSteps(row.action, row.config);
|
const t = AUTOMATION_TRIGGER_LABELS[row.trigger] ?? row.trigger;
|
||||||
if (chain.length <= 1) {
|
const a = AUTOMATION_ACTION_LABELS[row.action] ?? row.action;
|
||||||
return `${AUTOMATION_TRIGGER_LABELS[row.trigger]} → ${AUTOMATION_ACTION_LABELS[row.action]}`;
|
return `${t} ← ${a}`;
|
||||||
}
|
|
||||||
return `${AUTOMATION_TRIGGER_LABELS[row.trigger]} → ${toFaDigits(chain.length)} عمل`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -184,8 +197,9 @@ async function load() {
|
|||||||
items.value = rulesRes.data;
|
items.value = rulesRes.data;
|
||||||
users.value = usersRes.data;
|
users.value = usersRes.data;
|
||||||
if (editingId.value) {
|
if (editingId.value) {
|
||||||
const current = items.value.find((r) => r.id === editingId.value);
|
const cur = items.value.find((x) => x.id === editingId.value);
|
||||||
if (current && !dirty.value) hydrate(current);
|
if (cur && !dirty.value) hydrate(cur);
|
||||||
|
else if (!cur) hydrate(items.value[0] ?? null);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = errText(e, 'بارگذاری اتوماسیون ناموفق بود.');
|
error.value = errText(e, 'بارگذاری اتوماسیون ناموفق بود.');
|
||||||
@@ -196,7 +210,7 @@ async function load() {
|
|||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
const action = firstAutomationAction(steps.value);
|
const action = firstAutomationAction(steps.value);
|
||||||
if (!canSave.value || !action) {
|
if (!name.value.trim() || !action) {
|
||||||
error.value = !action ? 'حداقل یک عمل در زنجیره لازم است.' : 'نام قانون را وارد کنید.';
|
error.value = !action ? 'حداقل یک عمل در زنجیره لازم است.' : 'نام قانون را وارد کنید.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -279,31 +293,31 @@ watch(trigger, (next) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="crm-page">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="head-copy">
|
title="اتوماسیون"
|
||||||
<h1>اتوماسیون گردش کار</h1>
|
lead="مثل یک بوم کاری: گره «اگر» را به چند عمل پشتسرهم وصل کنید."
|
||||||
<p class="lead">
|
>
|
||||||
مثل یک بوم کاری: گره «اگر» را به چند عمل پشتسرهم وصل کنید.
|
<template #actions>
|
||||||
</p>
|
<UiButton variant="ghost" :disabled="loading" @click="load">
|
||||||
</div>
|
|
||||||
<div class="head-actions">
|
|
||||||
<button class="btn ghost" type="button" :disabled="loading" @click="load">
|
|
||||||
بروزرسانی
|
بروزرسانی
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn primary" type="button" @click="startNew">
|
<UiButton variant="primary" @click="startNew">
|
||||||
<UiIcon name="plus" :size="16" />
|
<UiIcon name="plus" :size="16" />
|
||||||
قانون جدید
|
قانون جدید
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</template>
|
||||||
</header>
|
</UiPageHeader>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error-msg">{{ error }}</p>
|
||||||
<p v-if="ok" class="ok">{{ ok }}</p>
|
<p v-if="ok" class="ok-msg">{{ ok }}</p>
|
||||||
|
|
||||||
<div class="workspace">
|
<div class="workspace">
|
||||||
<aside class="rules">
|
<aside class="rules">
|
||||||
<h2>قوانین</h2>
|
<div class="rules-head">
|
||||||
|
<h2>قوانین</h2>
|
||||||
|
<UiBadge variant="neutral" size="sm">{{ items.length }} قانون</UiBadge>
|
||||||
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="row in items"
|
v-for="row in items"
|
||||||
@@ -311,16 +325,21 @@ watch(trigger, (next) => {
|
|||||||
:class="{ on: editingId === row.id }"
|
:class="{ on: editingId === row.id }"
|
||||||
>
|
>
|
||||||
<button class="pick" type="button" @click="openRule(row)">
|
<button class="pick" type="button" @click="openRule(row)">
|
||||||
<strong>{{ row.name }}</strong>
|
<div class="pick-top">
|
||||||
<span>{{ chainLabel(row) }}</span>
|
<strong>{{ row.name }}</strong>
|
||||||
|
<UiBadge :variant="row.isActive ? 'success' : 'neutral'" size="sm" dot>
|
||||||
|
{{ row.isActive ? 'فعال' : 'غیرفعال' }}
|
||||||
|
</UiBadge>
|
||||||
|
</div>
|
||||||
|
<span class="chain-preview">{{ chainLabel(row) }}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="mini">
|
<div class="mini">
|
||||||
<button type="button" @click="toggleActive(row)">
|
<UiButton variant="ghost" size="sm" @click="toggleActive(row)">
|
||||||
{{ row.isActive ? 'فعال' : 'غیرفعال' }}
|
{{ row.isActive ? 'غیرفعالسازی' : 'فعالسازی' }}
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="danger" type="button" @click="removeRule(row.id)">
|
<UiButton variant="danger" size="sm" @click="removeRule(row.id)">
|
||||||
حذف
|
حذف
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -329,22 +348,23 @@ watch(trigger, (next) => {
|
|||||||
|
|
||||||
<div class="studio">
|
<div class="studio">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<label class="name">
|
<div class="name-field">
|
||||||
نام قانون
|
<span class="name-label">نام قانون:</span>
|
||||||
<input v-model="name" type="text" required minlength="2" maxlength="120" />
|
<UiInput v-model="name" required minlength="2" maxlength="120" placeholder="نام قانون اتوماسیون…" />
|
||||||
</label>
|
</div>
|
||||||
<label class="check">
|
<label class="check">
|
||||||
<input v-model="isActive" type="checkbox" />
|
<input v-model="isActive" type="checkbox" />
|
||||||
فعال
|
<span>فعال</span>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<UiButton
|
||||||
class="btn primary"
|
variant="primary"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="saving || !canSave"
|
:disabled="saving || !canSave"
|
||||||
|
:loading="saving"
|
||||||
@click="save"
|
@click="save"
|
||||||
>
|
>
|
||||||
{{ saving ? 'در حال ذخیره…' : editingId ? 'ذخیره' : 'ساخت' }}
|
{{ saving ? 'در حال ذخیره…' : editingId ? 'ذخیره قانون' : 'ساخت قانون' }}
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stage">
|
<div class="stage">
|
||||||
@@ -369,69 +389,53 @@ watch(trigger, (next) => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.error-msg {
|
||||||
display: grid;
|
color: var(--color-danger);
|
||||||
gap: 1rem;
|
font-size: var(--fs-sm);
|
||||||
min-width: 0;
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-head {
|
.ok-msg {
|
||||||
display: flex;
|
color: var(--color-success);
|
||||||
justify-content: space-between;
|
font-size: var(--fs-sm);
|
||||||
gap: 1rem;
|
font-weight: 600;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.head-copy {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.head-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.55rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 0.3rem;
|
|
||||||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lead,
|
|
||||||
.muted {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #64748b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 0.9rem;
|
gap: var(--sp-4);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rules,
|
.rules,
|
||||||
.studio {
|
.studio {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 18px;
|
border-radius: var(--radius-xl);
|
||||||
background: #fff;
|
background: var(--color-surface);
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rules {
|
.rules {
|
||||||
padding: 1rem;
|
padding: var(--sp-4);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.7rem;
|
gap: var(--sp-3);
|
||||||
align-content: start;
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.rules-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-head h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1rem;
|
font-size: var(--fs-base);
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rules ul {
|
.rules ul {
|
||||||
@@ -439,7 +443,7 @@ h2 {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.45rem;
|
gap: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rules ul:empty {
|
.rules ul:empty {
|
||||||
@@ -447,21 +451,22 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rules li {
|
.rules li {
|
||||||
border: 1px solid #e8eef3;
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 14px;
|
border-radius: var(--radius-lg);
|
||||||
padding: 0.55rem 0.65rem 0.45rem;
|
padding: var(--sp-3);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.35rem;
|
gap: var(--sp-2);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rules li.on {
|
.rules li.on {
|
||||||
border-color: #99f6e4;
|
border-color: var(--color-primary);
|
||||||
background: #f0fdfa;
|
background: var(--color-primary-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pick {
|
.pick {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.15rem;
|
gap: var(--sp-1);
|
||||||
text-align: start;
|
text-align: start;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -469,80 +474,76 @@ h2 {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pick strong {
|
.pick-top {
|
||||||
font-size: 0.9rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pick span {
|
.pick-top strong {
|
||||||
color: #64748b;
|
font-size: var(--fs-sm);
|
||||||
font-size: 0.75rem;
|
color: var(--color-text-emphasis);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-preview {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini {
|
.mini {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.65rem;
|
gap: var(--sp-2);
|
||||||
}
|
justify-content: flex-end;
|
||||||
|
border-top: 1px dashed var(--color-border);
|
||||||
.mini button {
|
padding-top: var(--sp-2);
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
color: #0f766e;
|
|
||||||
cursor: pointer;
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mini .danger {
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio {
|
.studio {
|
||||||
padding: 0.85rem;
|
padding: var(--sp-4);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: var(--sp-3);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.7rem;
|
gap: var(--sp-3);
|
||||||
align-items: end;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name-field {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 0.3rem;
|
align-items: center;
|
||||||
font-size: 0.82rem;
|
gap: var(--sp-2);
|
||||||
color: #64748b;
|
flex: 1 1 16rem;
|
||||||
flex: 1 1 14rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.name input {
|
.name-label {
|
||||||
border: 1px solid #e2e8f0;
|
font-size: var(--fs-sm);
|
||||||
border-radius: 12px;
|
font-weight: 600;
|
||||||
padding: 0.55rem 0.7rem;
|
color: var(--color-text-muted);
|
||||||
font: inherit;
|
white-space: nowrap;
|
||||||
color: #0f172a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.check {
|
.check {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: var(--sp-2);
|
||||||
color: #334155;
|
color: var(--color-text);
|
||||||
font-size: 0.88rem;
|
font-size: var(--fs-sm);
|
||||||
padding-bottom: 0.45rem;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage {
|
.stage {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 0.75rem;
|
gap: var(--sp-3);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,46 +552,9 @@ h2 {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.muted {
|
||||||
display: inline-flex;
|
color: var(--color-text-muted);
|
||||||
align-items: center;
|
font-size: var(--fs-sm);
|
||||||
justify-content: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.95rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 700;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.primary {
|
|
||||||
background: #14b8a6;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.primary:disabled {
|
|
||||||
opacity: 0.55;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.ghost {
|
|
||||||
background: #fff;
|
|
||||||
color: #0f766e;
|
|
||||||
border-color: #99f6e4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
margin: 0;
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ok {
|
|
||||||
margin: 0;
|
|
||||||
color: #0f766e;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
@@ -624,7 +588,7 @@ h2 {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
grid-area: toolbar;
|
grid-area: toolbar;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
||||||
padding: 0.85rem 1rem;
|
padding: 0.85rem 1rem;
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ import { RouterLink } from 'vue-router';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { formatMoney } from '@/lib/money';
|
import { formatMoney } from '@/lib/money';
|
||||||
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||||
import UiButton from '@/components/ui/UiButton.vue';
|
import {
|
||||||
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiChart,
|
||||||
|
UiEmptyState,
|
||||||
|
UiPageHeader,
|
||||||
|
type UiChartOption,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import type { CommissionEntryDto } from '@mento/shared';
|
import type { CommissionEntryDto } from '@mento/shared';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -13,6 +22,65 @@ const items = ref<CommissionEntryDto[]>([]);
|
|||||||
|
|
||||||
const posted = computed(() => items.value.filter((r) => r.status === 'posted'));
|
const posted = computed(() => items.value.filter((r) => r.status === 'posted'));
|
||||||
const total = computed(() => posted.value.reduce((sum, r) => sum + r.amount, 0));
|
const total = computed(() => posted.value.reduce((sum, r) => sum + r.amount, 0));
|
||||||
|
const avgCommission = computed(() => (posted.value.length ? Math.round(total.value / posted.value.length) : 0));
|
||||||
|
|
||||||
|
const chartOption = computed<UiChartOption>(() => {
|
||||||
|
const userMap = new Map<string, number>();
|
||||||
|
for (const r of posted.value) {
|
||||||
|
userMap.set(r.userName, (userMap.get(r.userName) || 0) + r.amount);
|
||||||
|
}
|
||||||
|
const names = Array.from(userMap.keys());
|
||||||
|
const values = Array.from(userMap.values());
|
||||||
|
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
formatter: (params: unknown) => {
|
||||||
|
const p = (params as Array<{ name: string; value: number }>)[0];
|
||||||
|
if (!p) return '';
|
||||||
|
return `${p.name}: <b>${formatMoney(p.value)}</b>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: 20,
|
||||||
|
right: 20,
|
||||||
|
bottom: 40,
|
||||||
|
left: 60,
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: names,
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#64748b',
|
||||||
|
},
|
||||||
|
axisLine: { lineStyle: { color: '#e2e8f0' } },
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#64748b',
|
||||||
|
formatter: (val: number) => (val >= 1000000 ? `${toFaDigits(String(val / 1000000))}M` : toFaDigits(String(val))),
|
||||||
|
},
|
||||||
|
splitLine: { lineStyle: { color: '#f1f5f9' } },
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'پورسانت',
|
||||||
|
type: 'bar',
|
||||||
|
barWidth: '36%',
|
||||||
|
data: values,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#0f766e',
|
||||||
|
borderRadius: [4, 4, 0, 0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -31,50 +99,99 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" dir="rtl">
|
<section class="crm-page commissions-page" dir="rtl">
|
||||||
<header class="head">
|
<UiPageHeader
|
||||||
<div>
|
title="پورسانت فروش"
|
||||||
<h1>پورسانت فروش</h1>
|
description="گزارش پورسانت ثبتشده برای کارشناسان فروش متصل به فاکتورهای صادره"
|
||||||
<p class="sub">با صدور هر فاکتور برای کارشناس مالک فاکتور ثبت میشود</p>
|
>
|
||||||
</div>
|
<template #actions>
|
||||||
<UiButton variant="ghost" type="button" :disabled="loading" @click="load">
|
<UiButton variant="ghost" :disabled="loading" @click="load">
|
||||||
بروزرسانی
|
<template #icon>
|
||||||
</UiButton>
|
<UiIcon name="refresh" />
|
||||||
</header>
|
</template>
|
||||||
|
بروزرسانی
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiPageHeader>
|
||||||
|
|
||||||
<article class="kpi">
|
<div class="kpi-grid">
|
||||||
<span>جمع پورسانت ثبتشده</span>
|
<UiCard class="kpi-card">
|
||||||
<strong>{{ formatMoney(total) }}</strong>
|
<div class="kpi-icon-wrap primary">
|
||||||
<small>{{ toFaDigits(String(posted.length)) }} فاکتور</small>
|
<UiIcon name="dollar" />
|
||||||
</article>
|
</div>
|
||||||
|
<div class="kpi-body">
|
||||||
|
<span class="kpi-label">جمع پورسانت ثبتشده</span>
|
||||||
|
<strong class="kpi-val">{{ formatMoney(total) }}</strong>
|
||||||
|
<small class="kpi-sub">{{ toFaDigits(String(posted.length)) }} فاکتور تایید شده</small>
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<UiCard class="kpi-card">
|
||||||
<p v-else-if="loading && !items.length" class="muted">در حال بارگذاری…</p>
|
<div class="kpi-icon-wrap success">
|
||||||
<p v-else-if="!items.length" class="muted">هنوز پورسانتی ثبت نشده.</p>
|
<UiIcon name="document" />
|
||||||
<div v-else class="table-wrap">
|
</div>
|
||||||
<table>
|
<div class="kpi-body">
|
||||||
|
<span class="kpi-label">تعداد تراکنشها</span>
|
||||||
|
<strong class="kpi-val">{{ toFaDigits(String(items.length)) }}</strong>
|
||||||
|
<small class="kpi-sub">{{ toFaDigits(String(posted.length)) }} فعال / {{ toFaDigits(String(items.length - posted.length)) }} باطل</small>
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard class="kpi-card">
|
||||||
|
<div class="kpi-icon-wrap warning">
|
||||||
|
<UiIcon name="trend" />
|
||||||
|
</div>
|
||||||
|
<div class="kpi-body">
|
||||||
|
<span class="kpi-label">میانگین هر فاکتور</span>
|
||||||
|
<strong class="kpi-val">{{ formatMoney(avgCommission) }}</strong>
|
||||||
|
<small class="kpi-sub">بر اساس موارد فعال</small>
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiCard v-if="posted.length > 0" title="سهم پورسانت به تفکیک کارشناسان" class="chart-card">
|
||||||
|
<UiChart :option="chartOption" height="260px" />
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||||||
|
|
||||||
|
<UiEmptyState
|
||||||
|
v-else-if="!loading && !items.length"
|
||||||
|
title="پورسانتی ثبت نشده است"
|
||||||
|
description="هنوز هیچ فاکتوری دارای پورسانت در سامانه ثبت نگردیده است."
|
||||||
|
icon="document"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else class="crm-table-container">
|
||||||
|
<table class="crm-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>کارشناس</th>
|
<th>کارشناس فروش</th>
|
||||||
<th>فاکتور</th>
|
<th>شماره و عنوان فاکتور</th>
|
||||||
<th>درصد</th>
|
<th>درصد</th>
|
||||||
<th>مبلغ</th>
|
<th>مبلغ پورسانت</th>
|
||||||
<th>وضعیت</th>
|
<th>وضعیت</th>
|
||||||
<th>تاریخ</th>
|
<th>تاریخ ثبت</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="row in items" :key="row.id">
|
<tr v-for="row in items" :key="row.id">
|
||||||
<td>{{ row.userName }}</td>
|
<td class="font-medium text-primary">{{ row.userName }}</td>
|
||||||
<td>
|
<td>
|
||||||
<RouterLink :to="{ name: 'invoice-detail', params: { id: row.invoiceId } }">
|
<RouterLink :to="{ name: 'invoice-detail', params: { id: row.invoiceId } }" class="table-link">
|
||||||
{{ row.invoiceNumber }}
|
{{ row.invoiceNumber }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<small>{{ row.invoiceTitle }}</small>
|
<span v-if="row.invoiceTitle" class="invoice-sub-title">{{ row.invoiceTitle }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<UiBadge variant="neutral">{{ toFaDigits(String(row.percent)) }}٪</UiBadge>
|
||||||
|
</td>
|
||||||
|
<td class="amount-cell">{{ formatMoney(row.amount, row.currency) }}</td>
|
||||||
|
<td>
|
||||||
|
<UiBadge :variant="row.status === 'posted' ? 'success' : 'neutral'">
|
||||||
|
{{ row.status === 'posted' ? 'ثبتشده' : 'باطل' }}
|
||||||
|
</UiBadge>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ toFaDigits(String(row.percent)) }}٪</td>
|
|
||||||
<td>{{ formatMoney(row.amount, row.currency) }}</td>
|
|
||||||
<td>{{ row.status === 'posted' ? 'ثبتشده' : 'باطل' }}</td>
|
|
||||||
<td>{{ formatJalaliNumeric(new Date(row.createdAt)) }}</td>
|
<td>{{ formatJalaliNumeric(new Date(row.createdAt)) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -84,21 +201,104 @@ onMounted(load);
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page { display: grid; gap: 1rem; }
|
.commissions-page {
|
||||||
.head { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; }
|
display: flex;
|
||||||
h1 { margin: 0; font-size: 1.25rem; }
|
flex-direction: column;
|
||||||
.sub, .muted { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.88rem; }
|
gap: var(--sp-6);
|
||||||
.error { color: var(--danger, #dc2626); font-weight: 600; }
|
}
|
||||||
.kpi {
|
|
||||||
background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
|
.kpi-grid {
|
||||||
padding: 1rem 1.1rem; width: fit-content; min-width: 16rem;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
|
||||||
|
gap: var(--sp-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
padding: var(--sp-4) var(--sp-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-icon-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-icon-wrap.primary {
|
||||||
|
background: var(--color-brand-50, #f0fdfa);
|
||||||
|
color: var(--color-brand-600, #0f766e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-icon-wrap.success {
|
||||||
|
background: var(--color-success-bg, #f0fdf4);
|
||||||
|
color: var(--color-success-text, #15803d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-icon-wrap.warning {
|
||||||
|
background: var(--color-warning-bg, #fffbeb);
|
||||||
|
color: var(--color-warning-text, #b45309);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-val {
|
||||||
|
font-size: var(--fs-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-sub {
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
margin-top: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-medium {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-cell {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-brand-700, #0f766e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-link {
|
||||||
|
color: var(--color-brand-600, #0f766e);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-sub-title {
|
||||||
|
display: block;
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.kpi span, .kpi small { display: block; color: var(--muted); font-size: 0.8rem; }
|
|
||||||
.kpi strong { display: block; margin: 0.25rem 0; font-size: 1.15rem; }
|
|
||||||
.table-wrap { overflow: auto; background: #fff; border: 1px solid var(--line); border-radius: var(--radius); }
|
|
||||||
table { width: 100%; border-collapse: collapse; min-width: 720px; }
|
|
||||||
th, td { padding: 0.7rem 0.65rem; border-bottom: 1px solid #eef2f7; text-align: right; vertical-align: top; }
|
|
||||||
th { background: #f8fafc; color: #64748b; font-size: 0.82rem; }
|
|
||||||
td small { display: block; color: var(--muted); font-size: 0.78rem; }
|
|
||||||
a { color: #0f766e; font-weight: 700; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ import { api } from '@/lib/api';
|
|||||||
import type { CompanyDto, CreateCompanyRequest } from '@mento/shared';
|
import type { CompanyDto, CreateCompanyRequest } from '@mento/shared';
|
||||||
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
|
import CustomFieldsSection from '@/components/CustomFieldsSection.vue';
|
||||||
import EntityTimeline from '@/components/EntityTimeline.vue';
|
import EntityTimeline from '@/components/EntityTimeline.vue';
|
||||||
|
import {
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiField,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
UiTextarea,
|
||||||
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -134,301 +142,187 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" :class="{ embedded }">
|
<section class="crm-page company-form-page" :class="{ embedded }" dir="rtl">
|
||||||
<div v-if="embedded" class="embed-actions">
|
<UiPageHeader
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="submit">
|
v-if="!embedded"
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
:title="pageTitle"
|
||||||
</button>
|
description="ثبت و ویرایش مشخصات حقوقی، اطلاعات تماس و فیلدهای سفارشی شرکت"
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
|
>
|
||||||
</div>
|
<template #breadcrumb>
|
||||||
<header v-if="!embedded" class="desktop-head">
|
<RouterLink :to="{ name: 'contacts', query: { tab: 'companies' } }" class="breadcrumb-link">
|
||||||
<div class="title-block">
|
شرکتها
|
||||||
<span class="doc-ico"><UiIcon name="building" :size="22" /></span>
|
</RouterLink>
|
||||||
<div>
|
<span class="breadcrumb-separator">/</span>
|
||||||
<h1>{{ pageTitle }}</h1>
|
<span>{{ pageTitle }}</span>
|
||||||
<p class="crumb">
|
</template>
|
||||||
<RouterLink :to="{ name: 'contacts', query: { tab: 'companies' } }">شرکتها</RouterLink>
|
|
||||||
<span>›</span>
|
<template #actions>
|
||||||
<strong>{{ pageTitle }}</strong>
|
<UiButton :loading="saving" @click="submit">
|
||||||
</p>
|
<template #icon>
|
||||||
</div>
|
<UiIcon name="check-plain" />
|
||||||
</div>
|
</template>
|
||||||
<div class="desk-actions">
|
ذخیره
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="submit">
|
</UiButton>
|
||||||
<UiIcon name="check-plain" :size="16" />
|
<UiButton variant="ghost" :disabled="saving" @click="cancel">
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
انصراف
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
|
<UiButton v-if="!isNew" variant="danger" :disabled="saving" @click="remove">
|
||||||
<button v-if="!isNew" class="btn danger" type="button" :disabled="saving" @click="remove">
|
<template #icon>
|
||||||
|
<UiIcon name="trash" />
|
||||||
|
</template>
|
||||||
حذف
|
حذف
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</template>
|
||||||
</header>
|
</UiPageHeader>
|
||||||
|
|
||||||
<header v-if="!embedded" class="mobile-head">
|
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||||||
<button class="icon-btn" type="button" aria-label="ذخیره" :disabled="saving" @click="submit">
|
|
||||||
<UiIcon name="check-plain" :size="22" />
|
|
||||||
</button>
|
|
||||||
<h1>{{ pageTitle }}</h1>
|
|
||||||
<button class="icon-btn" type="button" aria-label="بستن" @click="cancel">
|
|
||||||
<UiIcon name="x" :size="20" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
<form class="crm-form" @submit.prevent="submit">
|
||||||
<form v-else class="form" @submit.prevent="submit">
|
<UiCard title="اطلاعات اصلی شرکت" class="section-card">
|
||||||
<section class="card">
|
<div class="crm-form-grid-2">
|
||||||
<div class="grid">
|
<UiField label="نام شرکت" required class="col-span-2">
|
||||||
<div class="field wide">
|
<UiInput
|
||||||
<label for="name">نام شرکت</label>
|
id="name"
|
||||||
<input id="name" v-model="name" required minlength="2" />
|
v-model="name"
|
||||||
</div>
|
required
|
||||||
<div class="field">
|
minlength="2"
|
||||||
<label for="nationalId">شناسه ملی</label>
|
placeholder="مثال: دادهپردازی سپهر"
|
||||||
<input id="nationalId" v-model="nationalId" />
|
/>
|
||||||
</div>
|
</UiField>
|
||||||
<div class="field">
|
|
||||||
<label for="phone">تلفن</label>
|
<UiField label="شناسه ملی">
|
||||||
<input id="phone" v-model="phone" inputmode="tel" />
|
<UiInput
|
||||||
</div>
|
id="nationalId"
|
||||||
<div class="field">
|
v-model="nationalId"
|
||||||
<label for="email">ایمیل</label>
|
placeholder="شناسه ملی ۱۱ رقمی"
|
||||||
<input id="email" v-model="email" type="email" />
|
dir="ltr"
|
||||||
</div>
|
/>
|
||||||
<div class="field">
|
</UiField>
|
||||||
<label for="website">وبسایت</label>
|
|
||||||
<input id="website" v-model="website" />
|
<UiField label="شماره تلفن شرکت">
|
||||||
</div>
|
<UiInput
|
||||||
<div class="field wide">
|
id="phone"
|
||||||
<label for="address">آدرس</label>
|
v-model="phone"
|
||||||
<input id="address" v-model="address" />
|
type="tel"
|
||||||
</div>
|
placeholder="۰۲۱..."
|
||||||
<div class="field wide">
|
dir="ltr"
|
||||||
<label for="notes">یادداشت</label>
|
/>
|
||||||
<textarea id="notes" v-model="notes" rows="4" />
|
</UiField>
|
||||||
</div>
|
|
||||||
|
<UiField label="پست الکترونیکی (ایمیل)">
|
||||||
|
<UiInput
|
||||||
|
id="email"
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="info@company.ir"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="وبسایت">
|
||||||
|
<UiInput
|
||||||
|
id="website"
|
||||||
|
v-model="website"
|
||||||
|
placeholder="https://..."
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="آدرس پستی" class="col-span-2">
|
||||||
|
<UiInput
|
||||||
|
id="address"
|
||||||
|
v-model="address"
|
||||||
|
placeholder="نشانی، طبقه، پلاک..."
|
||||||
|
/>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="یادداشت و توضیحات تکمیلی" class="col-span-2">
|
||||||
|
<UiTextarea
|
||||||
|
id="notes"
|
||||||
|
v-model="notes"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="توضیحات مهم، حوزههای همکاری..."
|
||||||
|
/>
|
||||||
|
</UiField>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</UiCard>
|
||||||
|
|
||||||
<CustomFieldsSection entity-type="company" v-model="customFields" />
|
<CustomFieldsSection entity-type="company" v-model="customFields" />
|
||||||
<EntityTimeline v-if="!isNew && id" entity-type="company" :entity-id="id" />
|
<EntityTimeline v-if="!isNew && id" entity-type="company" :entity-id="id" />
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<div class="form-bottom-actions">
|
||||||
|
<UiButton type="submit" :loading="saving">
|
||||||
<div class="mobile-actions">
|
<template #icon>
|
||||||
<button class="btn primary" type="submit" :disabled="saving">
|
<UiIcon name="check-plain" />
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
</template>
|
||||||
</button>
|
ذخیره اطلاعات
|
||||||
<button v-if="!isNew" class="btn danger" type="button" :disabled="saving" @click="remove">
|
</UiButton>
|
||||||
حذف
|
<UiButton variant="ghost" type="button" :disabled="saving" @click="cancel">
|
||||||
</button>
|
انصراف
|
||||||
|
</UiButton>
|
||||||
|
<UiButton v-if="!isNew" variant="danger" type="button" :disabled="saving" @click="remove">
|
||||||
|
حذف شرکت
|
||||||
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.company-form-page {
|
||||||
display: grid;
|
|
||||||
gap: 0.85rem;
|
|
||||||
max-width: 820px;
|
|
||||||
min-width: 0;
|
|
||||||
padding-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page.embedded {
|
|
||||||
max-width: none;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: var(--sp-6);
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-head {
|
|
||||||
display: none;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-ico {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 14px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: #ecfdf5;
|
|
||||||
color: #0f766e;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crumb {
|
|
||||||
margin: 0.2rem 0 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crumb a {
|
|
||||||
color: #0f766e;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desk-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-head {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 40px 1fr 40px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-head h1 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn {
|
|
||||||
width: var(--control-h);
|
|
||||||
height: var(--control-h);
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
color: #0f766e;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 0.95rem;
|
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field.wide {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field label {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field input,
|
|
||||||
.field select,
|
|
||||||
.field textarea {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #e2e8f0;
|
max-width: 100%;
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.85rem;
|
|
||||||
font: inherit;
|
|
||||||
background: #fff;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.field textarea {
|
.company-form-page.embedded {
|
||||||
min-height: 5.5rem;
|
max-width: none;
|
||||||
padding: 0.65rem 0.85rem;
|
gap: var(--sp-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.embedded-actions {
|
||||||
color: #64748b;
|
display: flex;
|
||||||
margin: 0;
|
gap: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.breadcrumb-link {
|
||||||
color: #b91c1c;
|
color: var(--color-text-secondary);
|
||||||
margin: 0;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.breadcrumb-link:hover {
|
||||||
display: inline-flex;
|
color: var(--color-brand-600);
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.95rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 700;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.breadcrumb-separator {
|
||||||
background: #14b8a6;
|
margin: 0 var(--sp-2);
|
||||||
color: #fff;
|
color: var(--color-text-muted);
|
||||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.cancel {
|
.section-card {
|
||||||
background: #fff;
|
padding: var(--sp-6);
|
||||||
color: #64748b;
|
|
||||||
border-color: #e2e8f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.danger {
|
.col-span-2 {
|
||||||
background: #fff;
|
grid-column: span 2;
|
||||||
color: #b91c1c;
|
|
||||||
border-color: #fecaca;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-actions {
|
.form-bottom-actions {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: var(--sp-3);
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--sp-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 960px) {
|
@media (max-width: 640px) {
|
||||||
.desktop-head {
|
.col-span-2 {
|
||||||
display: flex;
|
grid-column: span 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-head,
|
.form-bottom-actions {
|
||||||
.mobile-actions {
|
flex-direction: column;
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,8 +5,13 @@ import axios from 'axios';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||||
import CustomerMoodFace from '@/components/CustomerMoodFace.vue';
|
import CustomerMoodFace from '@/components/CustomerMoodFace.vue';
|
||||||
import UiButton from '@/components/ui/UiButton.vue';
|
import {
|
||||||
import UiModal from '@/components/ui/UiModal.vue';
|
UiButton,
|
||||||
|
UiModal,
|
||||||
|
UiPageHeader,
|
||||||
|
UiPagination,
|
||||||
|
UiEmptyState,
|
||||||
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import PersonFormView from '@/views/PersonFormView.vue';
|
import PersonFormView from '@/views/PersonFormView.vue';
|
||||||
import CompanyFormView from '@/views/CompanyFormView.vue';
|
import CompanyFormView from '@/views/CompanyFormView.vue';
|
||||||
@@ -57,18 +62,6 @@ const total = computed(() =>
|
|||||||
tab.value === 'people' ? people.value?.total ?? 0 : companies.value?.total ?? 0,
|
tab.value === 'people' ? people.value?.total ?? 0 : companies.value?.total ?? 0,
|
||||||
);
|
);
|
||||||
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)));
|
const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE)));
|
||||||
const pageFrom = computed(() => (total.value ? (page.value - 1) * PAGE_SIZE + 1 : 0));
|
|
||||||
const pageTo = computed(() => Math.min(page.value * PAGE_SIZE, total.value));
|
|
||||||
const pageList = computed(() => {
|
|
||||||
const count = pageCount.value;
|
|
||||||
const current = page.value;
|
|
||||||
const pages: number[] = [];
|
|
||||||
for (let i = 1; i <= count; i += 1) {
|
|
||||||
if (i === 1 || i === count || Math.abs(i - current) <= 2) pages.push(i);
|
|
||||||
else if (pages[pages.length - 1] !== -1) pages.push(-1);
|
|
||||||
}
|
|
||||||
return pages;
|
|
||||||
});
|
|
||||||
|
|
||||||
function moodOf(person: PersonDto): PersonMood {
|
function moodOf(person: PersonDto): PersonMood {
|
||||||
return person.mood || 'newcomer';
|
return person.mood || 'newcomer';
|
||||||
@@ -249,20 +242,19 @@ onMounted(load);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" @click="closePopups">
|
<section class="page" @click="closePopups">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="head-copy">
|
title="مخاطبین"
|
||||||
<h1>مخاطبین</h1>
|
lead="مدیریت و مشاهده اشخاص و شرکتها"
|
||||||
<p class="lead">مدیریت و مشاهده اشخاص و شرکتها</p>
|
>
|
||||||
</div>
|
<template #actions>
|
||||||
<div class="actions">
|
<UiButton variant="ghost" size="sm" :disabled="busy" @click.stop="exportExcel">
|
||||||
<button class="btn btn-ghost excel-btn" type="button" :disabled="busy" @click.stop="exportExcel">
|
|
||||||
<span class="btn-full">خروجی اکسل</span>
|
<span class="btn-full">خروجی اکسل</span>
|
||||||
<span class="btn-short">خروجی</span>
|
<span class="btn-short">خروجی</span>
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn btn-ghost excel-btn" type="button" :disabled="busy" @click.stop="pickImportFile">
|
<UiButton variant="ghost" size="sm" :disabled="busy" @click.stop="pickImportFile">
|
||||||
<span class="btn-full">ورود از اکسل</span>
|
<span class="btn-full">ورود از اکسل</span>
|
||||||
<span class="btn-short">ورود</span>
|
<span class="btn-short">ورود</span>
|
||||||
</button>
|
</UiButton>
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -271,14 +263,14 @@ onMounted(load);
|
|||||||
@change="onImportFile"
|
@change="onImportFile"
|
||||||
/>
|
/>
|
||||||
<UiButton
|
<UiButton
|
||||||
type="button"
|
variant="primary"
|
||||||
@click="contactModal = tab === 'companies' ? 'company' : 'person'"
|
@click="contactModal = tab === 'companies' ? 'company' : 'person'"
|
||||||
>
|
>
|
||||||
<UiIcon name="plus" :size="16" />
|
<UiIcon name="plus" :size="16" />
|
||||||
{{ tab === 'companies' ? 'شرکت جدید' : 'شخص جدید' }}
|
{{ tab === 'companies' ? 'شرکت جدید' : 'شخص جدید' }}
|
||||||
</UiButton>
|
</UiButton>
|
||||||
</div>
|
</template>
|
||||||
</header>
|
</UiPageHeader>
|
||||||
|
|
||||||
<nav class="tabs">
|
<nav class="tabs">
|
||||||
<button type="button" :class="{ on: tab === 'people' }" @click.stop="setTab('people')">
|
<button type="button" :class="{ on: tab === 'people' }" @click.stop="setTab('people')">
|
||||||
@@ -442,7 +434,11 @@ onMounted(load);
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="empty">شخصی ثبت نشده. اولین نفر را اضافه کنید.</p>
|
<UiEmptyState
|
||||||
|
v-else
|
||||||
|
title="شخصی ثبت نشده است."
|
||||||
|
description="اولین مخاطب یا مشتری را اضافه کنید."
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -491,24 +487,20 @@ onMounted(load);
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="empty">شرکتی ثبت نشده. اولین شرکت را اضافه کنید.</p>
|
<UiEmptyState
|
||||||
|
v-else
|
||||||
|
title="شرکتی ثبت نشده است."
|
||||||
|
description="اولین شرکت یا سازمان را اضافه کنید."
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<footer v-if="total" class="pager">
|
<UiPagination
|
||||||
<span>
|
v-if="total"
|
||||||
نمایش {{ toFaDigits(pageFrom) }} تا {{ toFaDigits(pageTo) }} از {{ toFaDigits(total) }} مورد
|
:page="page"
|
||||||
</span>
|
:total-pages="pageCount"
|
||||||
<div class="pages">
|
:total-items="total"
|
||||||
<button type="button" :disabled="page <= 1" @click="setPage(page - 1)">‹</button>
|
@update:page="setPage"
|
||||||
<template v-for="(n, idx) in pageList" :key="`${n}-${idx}`">
|
/>
|
||||||
<span v-if="n < 0">…</span>
|
|
||||||
<button v-else type="button" :class="{ on: n === page }" @click="setPage(n)">
|
|
||||||
{{ toFaDigits(n) }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<button type="button" :disabled="page >= pageCount" @click="setPage(page + 1)">›</button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<UiModal
|
<UiModal
|
||||||
:open="Boolean(contactModal)"
|
:open="Boolean(contactModal)"
|
||||||
@@ -633,7 +625,7 @@ h1 {
|
|||||||
|
|
||||||
.tabs .on {
|
.tabs .on {
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
box-shadow: inset 0 -2px 0 #14b8a6;
|
box-shadow: inset 0 -2px 0 #14b8a6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,34 +650,44 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
flex: 1 1 12rem;
|
flex: 1 1 14rem;
|
||||||
min-width: 0;
|
min-width: 180px;
|
||||||
|
max-width: 400px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
gap: 0.45rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 0 0.75rem;
|
padding: 0 0.75rem;
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
height: var(--control-h);
|
height: var(--control-h);
|
||||||
color: #94a3b8;
|
color: var(--muted);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search:focus-within {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search input {
|
.search input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
outline: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: #0f172a;
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool {
|
.tool {
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
height: var(--control-h);
|
height: var(--control-h);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--line);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 0 0.75rem;
|
padding: 0 0.75rem;
|
||||||
@@ -694,9 +696,16 @@ h1 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: #475569;
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--ink-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool:hover {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pop {
|
.pop {
|
||||||
@@ -712,7 +721,7 @@ h1 {
|
|||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.14);
|
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.14);
|
||||||
min-width: min(12rem, calc(100vw - 2rem));
|
min-width: min(12rem, calc(100vw - 2rem));
|
||||||
max-width: calc(100vw - 1.5rem);
|
max-width: calc(100vw - 1.5rem);
|
||||||
@@ -725,7 +734,7 @@ h1 {
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0.5rem 0.65rem;
|
padding: 0.5rem 0.65rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -751,7 +760,7 @@ h1 {
|
|||||||
|
|
||||||
.table-wrap {
|
.table-wrap {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
@@ -789,7 +798,7 @@ th {
|
|||||||
|
|
||||||
.name {
|
.name {
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
@@ -814,7 +823,7 @@ th {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
@@ -869,7 +878,7 @@ strong.newcomer {
|
|||||||
gap: 0.7rem;
|
gap: 0.7rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -922,7 +931,7 @@ strong.newcomer {
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0.28rem 0.6rem;
|
padding: 0.28rem 0.6rem;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -972,7 +981,7 @@ strong.newcomer {
|
|||||||
padding: 1.4rem;
|
padding: 1.4rem;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pager {
|
.pager {
|
||||||
@@ -997,7 +1006,7 @@ strong.newcomer {
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import axios from 'axios';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { CURRENCY_OPTIONS, formatMoney } from '@/lib/money';
|
import { CURRENCY_OPTIONS, formatMoney } from '@/lib/money';
|
||||||
import MoneyInput from '@/components/MoneyInput.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
|
import {
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiField,
|
||||||
|
UiPageHeader,
|
||||||
|
UiSelect,
|
||||||
|
} from '@/components/ui';
|
||||||
import type { OrganizationDto, UpdateOrgCurrenciesRequest } from '@mento/shared';
|
import type { OrganizationDto, UpdateOrgCurrenciesRequest } from '@mento/shared';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -82,7 +89,7 @@ async function save() {
|
|||||||
body,
|
body,
|
||||||
);
|
);
|
||||||
org.value = data;
|
org.value = data;
|
||||||
ok.value = 'تنظیمات ارز ذخیره شد.';
|
ok.value = 'تنظیمات ارز با موفقیت ذخیره شد.';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (axios.isAxiosError(e)) {
|
if (axios.isAxiosError(e)) {
|
||||||
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||||
@@ -101,223 +108,201 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="crm-page currency-page" dir="rtl">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="head-copy">
|
title="ارزها و نرخ تبدیل"
|
||||||
<h1>ارزها و نرخ تبدیل</h1>
|
description="ارز پایه برای تجمیع مبالغ و پیشبینیها استفاده میشود. نرخ یعنی چند واحد ارز پایه برابر با ۱ واحد ارز خارجی است."
|
||||||
<p class="lead">
|
/>
|
||||||
ارز پایه برای جمعها و پیشبینی استفاده میشود. نرخ یعنی چند واحد ارز پایه
|
|
||||||
برابر با ۱ واحد ارز خارجی است.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<div v-if="ok" class="crm-alert crm-alert-success">{{ ok }}</div>
|
||||||
<p v-if="ok" class="ok">{{ ok }}</p>
|
|
||||||
|
|
||||||
<form v-if="!loading" class="panel" @submit.prevent="save">
|
<UiCard v-if="!loading" class="section-card">
|
||||||
<div class="field">
|
<form class="crm-form" @submit.prevent="save">
|
||||||
<label>ارز پایه</label>
|
<UiField label="ارز پایه سیستم" hint="همه فاکتورها، پیشبینیها و آمار کل بر پایه این واحد ارزی محاسبه خواهند شد.">
|
||||||
<select v-model="baseCurrency" @change="onBaseChange">
|
<UiSelect v-model="baseCurrency" @change="onBaseChange">
|
||||||
<option
|
<option
|
||||||
v-for="c in CURRENCY_OPTIONS"
|
v-for="c in CURRENCY_OPTIONS"
|
||||||
:key="c.code"
|
:key="c.code"
|
||||||
:value="c.code"
|
:value="c.code"
|
||||||
>
|
>
|
||||||
{{ c.labelFa }} ({{ c.code }})
|
{{ c.labelFa }} ({{ c.code }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
</div>
|
</UiField>
|
||||||
|
|
||||||
<div class="field">
|
<UiField label="ارزهای فعال در فاکتورها و معاملات">
|
||||||
<label>ارزهای فعال</label>
|
<div class="currency-chips">
|
||||||
<div class="chips">
|
<label
|
||||||
<label
|
v-for="c in CURRENCY_OPTIONS"
|
||||||
v-for="c in CURRENCY_OPTIONS"
|
:key="c.code"
|
||||||
:key="c.code"
|
class="currency-chip"
|
||||||
class="chip"
|
:class="{ active: enabled.includes(c.code), locked: c.code === baseCurrency }"
|
||||||
:class="{ on: enabled.includes(c.code), locked: c.code === baseCurrency }"
|
>
|
||||||
>
|
<input
|
||||||
<input
|
type="checkbox"
|
||||||
type="checkbox"
|
:checked="enabled.includes(c.code)"
|
||||||
:checked="enabled.includes(c.code)"
|
:disabled="c.code === baseCurrency"
|
||||||
:disabled="c.code === baseCurrency"
|
@change="toggleCurrency(c.code)"
|
||||||
@change="toggleCurrency(c.code)"
|
/>
|
||||||
/>
|
<span class="chip-title">{{ c.labelFa }}</span>
|
||||||
{{ c.labelFa }}
|
<span class="chip-code font-mono">{{ c.code }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<div v-if="rateCurrencies.length" class="rates-section">
|
||||||
|
<h3 class="rates-title">نرخ برابری نسبت به {{ baseCurrency }}</h3>
|
||||||
|
<div class="rates-list">
|
||||||
|
<div v-for="code in rateCurrencies" :key="code" class="rate-row">
|
||||||
|
<span class="rate-unit">۱ {{ code }} =</span>
|
||||||
|
<div class="rate-input-wrap">
|
||||||
|
<MoneyInput
|
||||||
|
v-model="rates[code]"
|
||||||
|
:decimals="6"
|
||||||
|
:placeholder="`نرخ برابری به ${baseCurrency}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="rate-target">{{ baseCurrency }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="rates.USD" class="example-note">
|
||||||
|
نمونه محاسبه: ۱ دلار آمریکا ≈ <strong>{{ formatMoney(Number(rates.USD) || 0, baseCurrency) }}</strong>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="rateCurrencies.length" class="rates">
|
<div class="form-submit-row">
|
||||||
<h2>نرخ به {{ baseCurrency }}</h2>
|
<UiButton type="submit" :loading="saving">
|
||||||
<div v-for="code in rateCurrencies" :key="code" class="rate-row">
|
ذخیره تنظیمات ارز
|
||||||
<span>۱ {{ code }} =</span>
|
</UiButton>
|
||||||
<MoneyInput
|
|
||||||
v-model="rates[code]"
|
|
||||||
:decimals="6"
|
|
||||||
:placeholder="`نرخ به ${baseCurrency}`"
|
|
||||||
/>
|
|
||||||
<span>{{ baseCurrency }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p v-if="rates.USD" class="muted example">
|
</form>
|
||||||
مثال: ۱ دلار ≈
|
</UiCard>
|
||||||
{{ formatMoney(Number(rates.USD) || 0, baseCurrency) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn primary" type="submit" :disabled="saving">
|
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.currency-page {
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
max-width: 640px;
|
|
||||||
min-width: 0;
|
|
||||||
overflow-x: clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-head {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: var(--sp-6);
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.head-copy {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 0.3rem;
|
|
||||||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lead,
|
|
||||||
.muted {
|
|
||||||
margin: 0;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
|
||||||
padding: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
select,
|
|
||||||
input[type='number'] {
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.85rem;
|
|
||||||
font: inherit;
|
|
||||||
background: #fff;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chips {
|
.section-card {
|
||||||
|
padding: var(--sp-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.currency-chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.45rem;
|
gap: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
.currency-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: var(--sp-2);
|
||||||
border: 1px solid #e2e8f0;
|
padding: var(--sp-2) var(--sp-3);
|
||||||
border-radius: 999px;
|
border-radius: var(--radius-md);
|
||||||
padding: 0.4rem 0.75rem;
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-surface);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: inherit;
|
user-select: none;
|
||||||
font-size: 0.9rem;
|
transition: all var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip.on {
|
.currency-chip:hover {
|
||||||
border-color: #99f6e4;
|
border-color: var(--color-brand-400);
|
||||||
background: #f0fdfa;
|
|
||||||
color: #0f766e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip.locked {
|
.currency-chip.active {
|
||||||
opacity: 0.85;
|
border-color: var(--color-brand-500);
|
||||||
|
background: var(--color-brand-50, #f0fdfa);
|
||||||
|
color: var(--color-brand-800, #115e59);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rates {
|
.currency-chip.locked {
|
||||||
display: grid;
|
opacity: 0.8;
|
||||||
gap: 0.55rem;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rates h2 {
|
.chip-title {
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rate-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr auto;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.95rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 700;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.primary {
|
|
||||||
background: #14b8a6;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
margin: 0;
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ok {
|
|
||||||
margin: 0;
|
|
||||||
color: #0f766e;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
.chip-code {
|
||||||
.rate-row {
|
font-size: var(--fs-xs);
|
||||||
grid-template-columns: 1fr;
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rates-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding: var(--sp-4);
|
||||||
|
background: var(--color-surface-soft);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rates-title {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rates-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-unit {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
min-width: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-input-wrap {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-target {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-family: var(--font-mono, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-note {
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-note strong {
|
||||||
|
color: var(--color-brand-700, #0f766e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-submit-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--sp-2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
|
import {
|
||||||
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiEmptyState,
|
||||||
|
UiField,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
UiSelect,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import type {
|
import type {
|
||||||
CreateCustomFieldRequest,
|
CreateCustomFieldRequest,
|
||||||
CustomFieldDefinitionDto,
|
CustomFieldDefinitionDto,
|
||||||
@@ -13,7 +24,7 @@ const loading = ref(false);
|
|||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const fields = ref<CustomFieldDefinitionDto[]>([]);
|
const fields = ref<CustomFieldDefinitionDto[]>([]);
|
||||||
const filter = ref<'all' | CustomFieldEntity>('all');
|
const filter = ref<string>('all');
|
||||||
|
|
||||||
const label = ref('');
|
const label = ref('');
|
||||||
const entityType = ref<CustomFieldEntity>('person');
|
const entityType = ref<CustomFieldEntity>('person');
|
||||||
@@ -112,283 +123,256 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="crm-page narrow">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="head-copy">
|
title="فیلدهای سفارشی"
|
||||||
<h1>فیلدهای سفارشی</h1>
|
lead="فیلدهای اختصاصی سازمان را برای شخص، شرکت یا معامله تعریف کنید."
|
||||||
<p class="lead">
|
/>
|
||||||
فیلدهای اختصاصی سازمان را برای شخص، شرکت یا معامله تعریف کنید.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form class="panel" @submit.prevent="createField">
|
<form class="crm-form" @submit.prevent="createField">
|
||||||
<h2>فیلد جدید</h2>
|
<UiCard>
|
||||||
<div class="grid">
|
<template #header>
|
||||||
<div class="field">
|
<div class="card-title">
|
||||||
<label for="cf-label">عنوان</label>
|
<UiIcon name="settings" :size="18" />
|
||||||
<input id="cf-label" v-model="label" required placeholder="مثلاً صنعت" />
|
<h3>فیلد جدید</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="crm-form-grid-3">
|
||||||
|
<UiField label="عنوان" required for-id="cf-label">
|
||||||
|
<UiInput id="cf-label" v-model="label" required placeholder="مثلاً صنعت" />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="روی چه چیزی؟" for-id="cf-entity">
|
||||||
|
<UiSelect id="cf-entity" v-model="entityType">
|
||||||
|
<option value="person">شخص</option>
|
||||||
|
<option value="company">شرکت</option>
|
||||||
|
<option value="deal">معامله</option>
|
||||||
|
</UiSelect>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="نوع فیلد" for-id="cf-type">
|
||||||
|
<UiSelect id="cf-type" v-model="fieldType">
|
||||||
|
<option value="text">متن</option>
|
||||||
|
<option value="number">عدد</option>
|
||||||
|
<option value="date">تاریخ</option>
|
||||||
|
<option value="boolean">بله/خیر</option>
|
||||||
|
<option value="select">لیست انتخابی</option>
|
||||||
|
</UiSelect>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<div v-if="fieldType === 'select'" class="crm-col-span-full">
|
||||||
|
<UiField label="گزینهها (با کاما جدا کنید)" for-id="cf-options">
|
||||||
|
<UiInput
|
||||||
|
id="cf-options"
|
||||||
|
v-model="optionsRaw"
|
||||||
|
placeholder="مثلاً فناوری، تولید، خدمات"
|
||||||
|
/>
|
||||||
|
</UiField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="crm-col-span-full check-row">
|
||||||
|
<label class="check">
|
||||||
|
<input v-model="required" type="checkbox" />
|
||||||
|
<span>فیلد اجباری است</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<template #footer>
|
||||||
<label for="cf-entity">روی چه چیزی؟</label>
|
<UiButton
|
||||||
<select id="cf-entity" v-model="entityType">
|
variant="primary"
|
||||||
<option value="person">شخص</option>
|
type="submit"
|
||||||
<option value="company">شرکت</option>
|
:disabled="saving"
|
||||||
<option value="deal">معامله</option>
|
:loading="saving"
|
||||||
</select>
|
>
|
||||||
</div>
|
<UiIcon name="plus" :size="16" />
|
||||||
<div class="field">
|
{{ saving ? 'در حال ثبت…' : 'افزودن فیلد' }}
|
||||||
<label for="cf-type">نوع</label>
|
</UiButton>
|
||||||
<select id="cf-type" v-model="fieldType">
|
</template>
|
||||||
<option value="text">متن</option>
|
</UiCard>
|
||||||
<option value="number">عدد</option>
|
|
||||||
<option value="date">تاریخ</option>
|
|
||||||
<option value="boolean">بله/خیر</option>
|
|
||||||
<option value="select">لیست انتخابی</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field check-field">
|
|
||||||
<label class="check">
|
|
||||||
<input v-model="required" type="checkbox" />
|
|
||||||
اجباری
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div v-if="fieldType === 'select'" class="field wide">
|
|
||||||
<label for="cf-options">گزینهها (با کاما جدا کنید)</label>
|
|
||||||
<input
|
|
||||||
id="cf-options"
|
|
||||||
v-model="optionsRaw"
|
|
||||||
placeholder="مثلاً فناوری، تولید، خدمات"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn primary" type="submit" :disabled="saving">
|
|
||||||
{{ saving ? 'در حال ثبت…' : 'افزودن فیلد' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="crm-toolbar">
|
||||||
<select v-model="filter">
|
<UiSelect v-model="filter" class="filter-select">
|
||||||
<option value="all">همه</option>
|
<option value="all">همه موجودیتها</option>
|
||||||
<option value="person">شخص</option>
|
<option value="person">شخص</option>
|
||||||
<option value="company">شرکت</option>
|
<option value="company">شرکت</option>
|
||||||
<option value="deal">معامله</option>
|
<option value="deal">معامله</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
<button class="btn btn-ghost" type="button" @click="load">بروزرسانی</button>
|
<UiButton variant="ghost" type="button" @click="load">
|
||||||
|
<UiIcon name="refresh" :size="16" />
|
||||||
|
بروزرسانی
|
||||||
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error-msg">{{ error }}</p>
|
||||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
||||||
|
|
||||||
<div v-else class="list">
|
<div v-else class="list">
|
||||||
<article v-for="field in visible" :key="field.id" class="row">
|
<UiCard v-for="field in visible" :key="field.id" padding="sm" class="field-item-card">
|
||||||
<div>
|
<div class="field-item">
|
||||||
<strong>{{ field.label }}</strong>
|
<div class="field-meta">
|
||||||
<p class="meta">
|
<div class="field-title-line">
|
||||||
{{ entityLabels[field.entityType] }} · {{ typeLabels[field.fieldType] }}
|
<strong>{{ field.label }}</strong>
|
||||||
· کلید: <code>{{ field.key }}</code>
|
<UiBadge :variant="field.entityType === 'deal' ? 'warning' : field.entityType === 'company' ? 'info' : 'primary'" size="sm">
|
||||||
<span v-if="!field.isActive"> · غیرفعال</span>
|
{{ entityLabels[field.entityType] }}
|
||||||
</p>
|
</UiBadge>
|
||||||
|
<UiBadge variant="neutral" size="sm">
|
||||||
|
{{ typeLabels[field.fieldType] }}
|
||||||
|
</UiBadge>
|
||||||
|
<UiBadge v-if="field.required" variant="danger" size="sm">
|
||||||
|
اجباری
|
||||||
|
</UiBadge>
|
||||||
|
<UiBadge :variant="field.isActive ? 'success' : 'neutral'" size="sm" dot>
|
||||||
|
{{ field.isActive ? 'فعال' : 'غیرفعال' }}
|
||||||
|
</UiBadge>
|
||||||
|
</div>
|
||||||
|
<p class="meta-code">
|
||||||
|
کلید سیستمی: <code>{{ field.key }}</code>
|
||||||
|
<span v-if="field.options?.length" class="options-preview">
|
||||||
|
· گزینهها: {{ field.options.join('، ') }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<UiButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="toggleActive(field)"
|
||||||
|
>
|
||||||
|
{{ field.isActive ? 'غیرفعالسازی' : 'فعالسازی' }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
@click="removeField(field)"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
</UiCard>
|
||||||
<button class="btn btn-ghost" type="button" @click="toggleActive(field)">
|
|
||||||
{{ field.isActive ? 'غیرفعال' : 'فعال' }}
|
<UiEmptyState
|
||||||
</button>
|
v-if="!visible.length"
|
||||||
<button class="btn btn-danger" type="button" @click="removeField(field)">
|
icon="settings"
|
||||||
حذف
|
title="فیلدی تعریف نشده است"
|
||||||
</button>
|
description="هیچ فیلد سفارشی برای این موجودیت وجود ندارد."
|
||||||
</div>
|
/>
|
||||||
</article>
|
|
||||||
<p v-if="!visible.length" class="muted">هنوز فیلدی تعریف نشده است.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.card-title {
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
max-width: 920px;
|
|
||||||
min-width: 0;
|
|
||||||
overflow-x: clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-head {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: var(--sp-2);
|
||||||
align-items: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.head-copy {
|
.card-title h3 {
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 0.3rem;
|
|
||||||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lead,
|
|
||||||
.muted,
|
|
||||||
.meta {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #64748b;
|
font-size: var(--fs-base);
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
margin: 0;
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel,
|
|
||||||
.row {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
|
||||||
padding: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #0f172a;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.check-row {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wide {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field label {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
select,
|
|
||||||
input[type='text'] {
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.95rem;
|
|
||||||
background: #fff;
|
|
||||||
font: inherit;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-field {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: end;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check {
|
.check {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
gap: var(--sp-2);
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.6rem;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar select {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.95rem;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 700;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.filter-select {
|
||||||
background: #14b8a6;
|
min-width: 180px;
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-ghost {
|
.error-msg {
|
||||||
background: #fff;
|
color: var(--color-danger);
|
||||||
color: #0f766e;
|
font-size: var(--fs-sm);
|
||||||
border: 1px solid #99f6e4;
|
font-weight: 600;
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: #fff;
|
|
||||||
color: #b91c1c;
|
|
||||||
border: 1px solid #fecaca;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list {
|
.list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.7rem;
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.field-item-card {
|
||||||
|
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-item-card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta {
|
.field-meta {
|
||||||
margin-top: 0.25rem;
|
display: grid;
|
||||||
font-size: 0.92rem;
|
gap: var(--sp-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-title-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-title-line strong {
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
color: var(--color-text-emphasis);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-code {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-code code {
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
background: var(--color-surface-sunken);
|
||||||
|
padding: 1px var(--sp-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-preview {
|
||||||
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.45rem;
|
gap: var(--sp-2);
|
||||||
flex-wrap: wrap;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
.muted {
|
||||||
font-family: Consolas, monospace;
|
color: var(--color-text-muted);
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 640px) {
|
||||||
.grid {
|
.field-item {
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions,
|
.actions {
|
||||||
.toolbar {
|
justify-content: flex-end;
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+259
-252
@@ -12,6 +12,7 @@ import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
|||||||
import MoneyInput from '@/components/MoneyInput.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import UserAvatar from '@/components/UserAvatar.vue';
|
import UserAvatar from '@/components/UserAvatar.vue';
|
||||||
|
import { UiButton, UiField, UiInput, UiPageHeader, UiSelect } from '@/components/ui';
|
||||||
import type {
|
import type {
|
||||||
CompanyDto,
|
CompanyDto,
|
||||||
CreateDealRequest,
|
CreateDealRequest,
|
||||||
@@ -394,63 +395,45 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNotes() {
|
|
||||||
if (window.innerWidth < 960) tab.value = 'notes';
|
|
||||||
document.getElementById('notes')?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" :class="{ embedded }" @click="closeMenus">
|
<section class="page" :class="{ embedded }" @click="closeMenus">
|
||||||
<div v-if="embedded" class="embed-actions">
|
<UiPageHeader
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="submit">
|
v-if="!embedded"
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره معامله' }}
|
:title="pageTitle"
|
||||||
</button>
|
:back-to="{ name: 'pipeline' }"
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">
|
>
|
||||||
انصراف
|
<template #lead>
|
||||||
</button>
|
<span class="crumb">
|
||||||
</div>
|
<RouterLink :to="{ name: 'pipeline' }">کاریز فروش</RouterLink>
|
||||||
|
<span>›</span>
|
||||||
<header v-if="!embedded" class="desktop-head">
|
<strong>{{ pageTitle }}</strong>
|
||||||
<div class="title-block">
|
|
||||||
<span class="doc-ico">
|
|
||||||
<UiIcon name="document" :size="22" />
|
|
||||||
</span>
|
</span>
|
||||||
<div>
|
</template>
|
||||||
<h1>{{ pageTitle }}</h1>
|
<template #actions>
|
||||||
<p class="crumb">
|
|
||||||
<RouterLink :to="{ name: 'pipeline' }">کاریز فروش</RouterLink>
|
|
||||||
<span>›</span>
|
|
||||||
<strong>{{ pageTitle }}</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="desk-actions">
|
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="!isNew && id"
|
v-if="!isNew && id"
|
||||||
class="btn outline"
|
|
||||||
:to="{ name: 'quote-new', query: { fromDealId: id } }"
|
:to="{ name: 'quote-new', query: { fromDealId: id } }"
|
||||||
>
|
>
|
||||||
صدور پیشفاکتور
|
<UiButton variant="outline">صدور پیشفاکتور</UiButton>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-if="!isNew && id"
|
v-if="!isNew && id"
|
||||||
class="btn outline"
|
|
||||||
:to="{ name: 'invoice-new', query: { fromDealId: id } }"
|
:to="{ name: 'invoice-new', query: { fromDealId: id } }"
|
||||||
>
|
>
|
||||||
صدور فاکتور
|
<UiButton variant="outline">صدور فاکتور</UiButton>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="submit">
|
<UiButton variant="primary" :disabled="saving" @click="submit">
|
||||||
<UiIcon name="check-plain" :size="16" />
|
<UiIcon name="check" :size="16" />
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره معامله' }}
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره معامله' }}
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">
|
<UiButton variant="ghost" :disabled="saving" @click="cancel">
|
||||||
انصراف
|
انصراف
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</template>
|
||||||
</header>
|
</UiPageHeader>
|
||||||
|
|
||||||
<header v-if="!embedded" class="mobile-head">
|
<header v-if="!embedded" class="mobile-head">
|
||||||
<button class="icon-btn" type="button" aria-label="ذخیره" :disabled="saving" @click="submit">
|
<button class="icon-btn" type="button" aria-label="ذخیره" :disabled="saving" @click="submit">
|
||||||
@@ -645,120 +628,116 @@ onMounted(load);
|
|||||||
|
|
||||||
<section class="card extras">
|
<section class="card extras">
|
||||||
<button class="extras-toggle" type="button" @click="showOthers = !showOthers">
|
<button class="extras-toggle" type="button" @click="showOthers = !showOthers">
|
||||||
سایر فیلدها
|
<span class="extras-toggle-title">
|
||||||
|
<UiIcon name="layers" :size="18" />
|
||||||
|
سایر فیلدها و اطلاعات تکمیلی
|
||||||
|
</span>
|
||||||
<UiIcon name="chevron" :size="16" :class="{ open: showOthers }" />
|
<UiIcon name="chevron" :size="16" :class="{ open: showOthers }" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="showOthers" class="extras-grid">
|
<div v-if="showOthers" class="extras-body">
|
||||||
<label class="mini">
|
<div class="crm-form-grid-2">
|
||||||
<span class="mini-ico"><UiIcon name="dollar" :size="16" /></span>
|
<UiField label="واحد ارزی">
|
||||||
<span class="mini-copy">
|
<UiSelect v-model="currency">
|
||||||
<small>واحد ارزی</small>
|
|
||||||
<select v-model="currency">
|
|
||||||
<option v-for="code in enabledCurrencies" :key="code" :value="code">
|
<option v-for="code in enabledCurrencies" :key="code" :value="code">
|
||||||
{{ currencySuffix(code) }}
|
{{ currencySuffix(code) }} ({{ code }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
</span>
|
</UiField>
|
||||||
<UiIcon name="chevron" :size="14" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button class="mini" type="button" @click="productsOpen = !productsOpen">
|
<UiField label="کاریز فروش">
|
||||||
<span class="mini-ico"><UiIcon name="box" :size="16" /></span>
|
<UiSelect :model-value="pipelineId" @update:model-value="onPipelineChange($event)">
|
||||||
<span class="mini-copy">
|
|
||||||
<small>لیست محصولات</small>
|
|
||||||
<strong>{{ productsLabel }}</strong>
|
|
||||||
</span>
|
|
||||||
<UiIcon name="chevron" :size="14" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<label class="mini">
|
|
||||||
<span class="mini-ico"><UiIcon name="flag" :size="16" /></span>
|
|
||||||
<span class="mini-copy">
|
|
||||||
<small>کاریز</small>
|
|
||||||
<select :value="pipelineId" @change="onPipelineChange(($event.target as HTMLSelectElement).value)">
|
|
||||||
<option v-for="p in pipelines" :key="p.id" :value="p.id">{{ p.name }}</option>
|
<option v-for="p in pipelines" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
</span>
|
</UiField>
|
||||||
<UiIcon name="chevron" :size="14" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="mini">
|
<UiField label="مرحله کاریز" required>
|
||||||
<span class="mini-ico"><UiIcon name="layers" :size="16" /></span>
|
<UiSelect v-model="stageId" required>
|
||||||
<span class="mini-copy">
|
|
||||||
<small>مرحله کاریز</small>
|
|
||||||
<select v-model="stageId" required>
|
|
||||||
<option v-for="s in stages" :key="s.id" :value="s.id">{{ s.name }}</option>
|
<option v-for="s in stages" :key="s.id" :value="s.id">{{ s.name }}</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
</span>
|
</UiField>
|
||||||
<UiIcon name="chevron" :size="14" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="mini">
|
<UiField label="وضعیت سلامت معامله">
|
||||||
<span class="mini-ico"><UiIcon name="target" :size="16" /></span>
|
<UiSelect v-model="health">
|
||||||
<span class="mini-copy">
|
|
||||||
<small>وضعیت معامله</small>
|
|
||||||
<select v-model="health">
|
|
||||||
<option value="normal">عادی</option>
|
<option value="normal">عادی</option>
|
||||||
<option value="stale">راکد</option>
|
<option value="stale">راکد</option>
|
||||||
<option value="rotten">فاسد</option>
|
<option value="rotten">فاسد</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
</span>
|
</UiField>
|
||||||
<UiIcon name="chevron" :size="14" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="mini">
|
<UiField label="شیوه آشنایی">
|
||||||
<span class="mini-ico"><UiIcon name="users" :size="16" /></span>
|
<UiInput v-model="source" placeholder="مثال: وبسایت، معرفی، نمایشگاه…" />
|
||||||
<span class="mini-copy">
|
</UiField>
|
||||||
<small>شیوه آشنایی</small>
|
|
||||||
<input v-model="source" placeholder="وبسایت، معرفی…" />
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="mini">
|
<UiField label="برچسبها">
|
||||||
<span class="mini-ico"><UiIcon name="filter" :size="16" /></span>
|
<UiInput v-model="tagsRaw" placeholder="سازمانی، فوری (با کاما جدا کنید)" />
|
||||||
<span class="mini-copy">
|
</UiField>
|
||||||
<small>برچسبها</small>
|
|
||||||
<input v-model="tagsRaw" placeholder="سازمانی، فوری" />
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button class="mini notes-mini" type="button" @click="openNotes">
|
<UiField v-if="!isNew" label="دلیل شکست (در صورت باخت معامله)" class="crm-col-span-full">
|
||||||
<span class="mini-ico"><UiIcon name="chat" :size="16" /></span>
|
<UiInput id="lostReason" v-model="lostReason" placeholder="علت از دست رفتن معامله را بنویسید…" />
|
||||||
<span class="mini-copy">
|
</UiField>
|
||||||
<small>توضیحات و یادداشت</small>
|
|
||||||
<strong>{{ notes.trim() ? 'ویرایش یادداشت' : 'افزودن یادداشت' }}</strong>
|
|
||||||
</span>
|
|
||||||
<UiIcon name="chevron" :size="14" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="showOthers && productsOpen" class="lines-block">
|
|
||||||
<div class="lines-head">
|
|
||||||
<h3>محصولات / خدمات</h3>
|
|
||||||
<button class="linkish" type="button" @click="addLine">افزودن ردیف</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-for="line in lines" :key="line.key" class="line-row">
|
|
||||||
<select v-model="line.productId" @change="onPickProduct(line)">
|
<!-- بخش محصولات / خدمات -->
|
||||||
<option value="">محصول از کاتالوگ…</option>
|
<div class="products-section">
|
||||||
<option v-for="p in products" :key="p.id" :value="p.id">{{ p.name }}</option>
|
<div class="products-head">
|
||||||
</select>
|
<div class="products-head-info">
|
||||||
<input v-model="line.name" placeholder="نام ردیف" />
|
<span class="sec-title">محصولات / خدمات</span>
|
||||||
<input v-model="line.quantity" type="number" min="0.01" step="0.01" placeholder="تعداد" />
|
<span class="lines-count-badge">{{ productsLabel }}</span>
|
||||||
<MoneyInput v-model="line.unitPrice" :decimals="0" placeholder="قیمت واحد" />
|
</div>
|
||||||
<button class="linkish danger" type="button" @click="removeLine(line.key)">حذف</button>
|
<div class="products-head-actions">
|
||||||
|
<UiButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
@click="productsOpen = !productsOpen"
|
||||||
|
>
|
||||||
|
<UiIcon name="box" :size="14" />
|
||||||
|
{{ productsOpen ? 'پنهان کردن اقلام' : 'مدیریت اقلام معامله' }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-if="productsOpen"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
@click="addLine"
|
||||||
|
>
|
||||||
|
<UiIcon name="plus" :size="14" />
|
||||||
|
افزودن ردیف
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="productsOpen" class="lines-block">
|
||||||
|
<div v-if="lines.length" class="lines-table">
|
||||||
|
<div class="lines-table-head">
|
||||||
|
<span>محصول از کاتالوگ</span>
|
||||||
|
<span>نام ردیف</span>
|
||||||
|
<span>تعداد</span>
|
||||||
|
<span>قیمت واحد ({{ currencyLabel }})</span>
|
||||||
|
<span>عملیات</span>
|
||||||
|
</div>
|
||||||
|
<div v-for="line in lines" :key="line.key" class="line-row">
|
||||||
|
<UiSelect v-model="line.productId" @change="onPickProduct(line)">
|
||||||
|
<option value="">محصول از کاتالوگ…</option>
|
||||||
|
<option v-for="p in products" :key="p.id" :value="p.id">{{ p.name }}</option>
|
||||||
|
</UiSelect>
|
||||||
|
<UiInput v-model="line.name" placeholder="نام ردیف" />
|
||||||
|
<UiInput v-model="line.quantity" type="number" min="0.01" step="0.01" placeholder="تعداد" />
|
||||||
|
<MoneyInput v-model="line.unitPrice" :decimals="0" placeholder="قیمت واحد" />
|
||||||
|
<button class="line-delete-btn" type="button" title="حذف ردیف" @click="removeLine(line.key)">
|
||||||
|
<UiIcon name="trash" :size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="lines-empty-hint">
|
||||||
|
میتوانید فقط مبلغ کل را در فیلد مبلغ وارد کنید یا با کلیک روی «افزودن ردیف»، اقلام را از کاتالوگ محصول مشخص فرمایید.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!lines.length" class="muted">
|
|
||||||
میتوانید فقط مبلغ کل را وارد کنید یا از کاتالوگ محصول ردیف اضافه کنید.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="showOthers && !isNew" class="field lost">
|
<CustomFieldsSection entity-type="deal" v-model="customFields" />
|
||||||
<label for="lostReason">دلیل شکست (اگر از دست رفت)</label>
|
|
||||||
<input id="lostReason" v-model="lostReason" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CustomFieldsSection v-if="showOthers" entity-type="deal" v-model="customFields" />
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -792,6 +771,16 @@ onMounted(load);
|
|||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره معامله' }}
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره معامله' }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- Embedded Modal Actions (Fixed at bottom) -->
|
||||||
|
<div v-if="embedded" class="embed-actions">
|
||||||
|
<UiButton variant="primary" :disabled="saving" @click="submit">
|
||||||
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره معامله' }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="ghost" :disabled="saving" @click="cancel">
|
||||||
|
انصراف
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -799,7 +788,8 @@ onMounted(load);
|
|||||||
.page {
|
.page {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
padding-bottom: 5.8rem;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,7 +819,7 @@ onMounted(load);
|
|||||||
.doc-ico {
|
.doc-ico {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: #ecfdf5;
|
background: #ecfdf5;
|
||||||
@@ -900,7 +890,7 @@ h1 {
|
|||||||
|
|
||||||
.tabs .on {
|
.tabs .on {
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
box-shadow: inset 0 -2px 0 #14b8a6;
|
box-shadow: inset 0 -2px 0 #14b8a6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -914,7 +904,7 @@ h1 {
|
|||||||
.card {
|
.card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
padding: 0.95rem;
|
padding: 0.95rem;
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1039,7 +1029,7 @@ h1 {
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.35rem 0.55rem;
|
padding: 0.35rem 0.55rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -1065,7 +1055,7 @@ h1 {
|
|||||||
min-height: calc(var(--control-h) - 4px);
|
min-height: calc(var(--control-h) - 4px);
|
||||||
height: calc(var(--control-h) - 4px);
|
height: calc(var(--control-h) - 4px);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1079,7 +1069,7 @@ h1 {
|
|||||||
.segment .on {
|
.segment .on {
|
||||||
background: #ecfdf5;
|
background: #ecfdf5;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
@@ -1091,7 +1081,7 @@ h1 {
|
|||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
||||||
max-height: 16rem;
|
max-height: 16rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -1110,7 +1100,7 @@ h1 {
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0.55rem 0.5rem;
|
padding: 0.55rem 0.5rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1127,7 +1117,7 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.who b {
|
.who b {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.who small {
|
.who small {
|
||||||
@@ -1150,7 +1140,7 @@ h1 {
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
background: #f0fdfa;
|
background: #f0fdfa;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1163,141 +1153,167 @@ h1 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border: 0;
|
border: 1px solid var(--line);
|
||||||
background: #f8fafc;
|
background: var(--surface-subtle);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius, 10px);
|
||||||
padding: 0.7rem 0.85rem;
|
padding: 0.75rem 1rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #0f172a;
|
color: var(--ink);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.extras-toggle:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.extras-toggle-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
font-size: var(--fs-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.extras-toggle :deep(.ico) {
|
.extras-toggle :deep(.ico) {
|
||||||
color: #0f766e;
|
color: var(--muted);
|
||||||
transform: rotate(-90deg);
|
transition: transform var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.extras-toggle :deep(.ico.open) {
|
.extras-toggle :deep(.ico.open) {
|
||||||
transform: rotate(90deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.extras-grid {
|
.extras-body {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 0.55rem;
|
flex-direction: column;
|
||||||
margin-top: 0.75rem;
|
gap: var(--sp-5);
|
||||||
|
margin-top: var(--sp-4);
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini {
|
.products-section {
|
||||||
min-height: var(--control-h);
|
display: flex;
|
||||||
height: var(--control-h);
|
flex-direction: column;
|
||||||
border: 1px solid #eef2f6;
|
gap: var(--sp-3);
|
||||||
border-radius: var(--radius);
|
padding-top: var(--sp-4);
|
||||||
background: #fff;
|
border-top: 1px dashed var(--line);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.65rem;
|
justify-content: space-between;
|
||||||
padding: 0.35rem 0.7rem;
|
flex-wrap: wrap;
|
||||||
font: inherit;
|
gap: var(--sp-2);
|
||||||
text-align: start;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-ico {
|
.products-head-info {
|
||||||
width: 32px;
|
display: flex;
|
||||||
height: 32px;
|
align-items: center;
|
||||||
border-radius: 10px;
|
gap: var(--sp-2);
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: #f0fdfa;
|
|
||||||
color: #0f766e;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-copy {
|
.sec-title {
|
||||||
display: grid;
|
font-size: var(--fs-sm);
|
||||||
min-width: 0;
|
font-weight: var(--fw-semibold);
|
||||||
flex: 1;
|
color: var(--ink-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-copy small {
|
.lines-count-badge {
|
||||||
color: #94a3b8;
|
font-size: var(--fs-xs);
|
||||||
font-size: 0.72rem;
|
color: var(--muted);
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-copy strong {
|
.products-head-actions {
|
||||||
font-size: 0.9rem;
|
display: flex;
|
||||||
font-weight: 700;
|
align-items: center;
|
||||||
}
|
gap: var(--sp-2);
|
||||||
|
|
||||||
.mini select,
|
|
||||||
.mini input {
|
|
||||||
width: 100%;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
font: inherit;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 0;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mini :deep(.ico:last-child) {
|
|
||||||
color: #cbd5e1;
|
|
||||||
transform: rotate(90deg);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lines-block {
|
.lines-block {
|
||||||
display: grid;
|
|
||||||
gap: 0.55rem;
|
|
||||||
margin-top: 0.85rem;
|
|
||||||
padding-top: 0.75rem;
|
|
||||||
border-top: 1px dashed #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lines-head {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lines-head h3 {
|
.lines-table {
|
||||||
margin: 0;
|
display: flex;
|
||||||
font-size: 0.92rem;
|
flex-direction: column;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lines-table-head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.5fr 1.5fr 0.8fr 1.2fr 40px;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
font-weight: var(--fw-medium);
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-row {
|
.line-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1.5fr 1.5fr 0.8fr 1.2fr 40px;
|
||||||
gap: 0.4rem;
|
gap: var(--sp-2);
|
||||||
}
|
align-items: center;
|
||||||
|
|
||||||
.line-row input,
|
|
||||||
.line-row select,
|
|
||||||
.lost input {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0.65rem 0.75rem;
|
|
||||||
background: #fff;
|
|
||||||
font: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lost {
|
.line-delete-btn {
|
||||||
margin-top: 0.75rem;
|
width: 36px;
|
||||||
}
|
height: 36px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
.linkish {
|
border-radius: var(--radius, 10px);
|
||||||
border: 0;
|
background: var(--surface);
|
||||||
background: none;
|
color: var(--danger);
|
||||||
color: #0f766e;
|
display: grid;
|
||||||
font: inherit;
|
place-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.linkish.danger {
|
.line-delete-btn:hover {
|
||||||
color: #b91c1c;
|
background: #fef2f2;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lines-empty-hint {
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--sp-3);
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border: 1px dashed var(--line);
|
||||||
|
border-radius: var(--radius, 10px);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.lines-table-head {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
padding: var(--sp-3);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius, 10px);
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-delete-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-card {
|
.note-card {
|
||||||
@@ -1314,7 +1330,7 @@ h1 {
|
|||||||
min-height: 12rem;
|
min-height: 12rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.75rem 0.85rem;
|
padding: 0.75rem 0.85rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
@@ -1345,7 +1361,7 @@ h1 {
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 0 0.95rem;
|
padding: 0 0.95rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1447,15 +1463,6 @@ h1 {
|
|||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.extras-grid {
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-row {
|
|
||||||
grid-template-columns: 1.2fr 1.4fr 0.7fr 0.9fr auto;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-card {
|
.note-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ import type {
|
|||||||
FaqArticleDto,
|
FaqArticleDto,
|
||||||
UpdateFaqArticleRequest,
|
UpdateFaqArticleRequest,
|
||||||
} from '@mento/shared';
|
} from '@mento/shared';
|
||||||
|
import {
|
||||||
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiEmptyState,
|
||||||
|
UiField,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
UiTextarea,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@@ -116,282 +127,204 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="crm-page faq-page" dir="rtl">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="head-copy">
|
title="پایگاه دانش و سؤالات متداول"
|
||||||
<h1>پایگاه دانش (FAQ)</h1>
|
description="سؤالات پرتکرار و پاسخهای جامع برای تسریع در پاسخگویی پشتیبانان و کاهش تیکتهای مکرر"
|
||||||
<p class="lead">سؤالات پرتکرار برای کاهش تیکت تکراری.</p>
|
/>
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||||||
<p v-if="ok" class="ok">{{ ok }}</p>
|
<div v-if="ok" class="crm-alert crm-alert-success">{{ ok }}</div>
|
||||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
|
||||||
|
|
||||||
<form class="panel" @submit.prevent="save">
|
<UiCard :title="editingId ? 'ویرایش مقاله' : 'مقاله جدید'" class="editor-card">
|
||||||
<h2>{{ editingId ? 'ویرایش مقاله' : 'مقاله جدید' }}</h2>
|
<form class="crm-form" @submit.prevent="save">
|
||||||
<div class="field">
|
<UiField label="پرسش / عنوان مقاله" required>
|
||||||
<label>سؤال</label>
|
<UiInput v-model="question" required maxlength="500" placeholder="مثلاً نحوه فعالسازی حساب کاربری..." />
|
||||||
<input v-model="question" required maxlength="500" />
|
</UiField>
|
||||||
</div>
|
|
||||||
<div class="field">
|
<UiField label="پاسخ کامل" required>
|
||||||
<label>پاسخ</label>
|
<UiTextarea v-model="answer" :rows="4" required placeholder="توضیحات و راهنمای گامبهگام..." />
|
||||||
<textarea v-model="answer" rows="4" required />
|
</UiField>
|
||||||
</div>
|
|
||||||
<div class="row-fields">
|
<div class="form-grid-extra">
|
||||||
<div class="field">
|
<UiField label="دستهبندی">
|
||||||
<label>دسته</label>
|
<UiInput v-model="category" maxlength="100" placeholder="مثلاً مالی، فنی، نصب" />
|
||||||
<input v-model="category" maxlength="100" placeholder="مثلاً نصب" />
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="ترتیب نمایش">
|
||||||
|
<UiInput v-model.number="position" type="number" min="0" />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<div class="publish-toggle-wrap">
|
||||||
|
<label class="publish-label">
|
||||||
|
<input v-model="isPublished" type="checkbox" />
|
||||||
|
<span>منتشرشده در پورتال</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label>ترتیب</label>
|
|
||||||
<input v-model.number="position" type="number" min="0" />
|
|
||||||
</div>
|
|
||||||
<label class="check">
|
|
||||||
<input v-model="isPublished" type="checkbox" />
|
|
||||||
منتشرشده
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button class="btn primary" type="submit" :disabled="saving">
|
|
||||||
{{ saving ? '…' : editingId ? 'ذخیره تغییرات' : 'افزودن' }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="editingId"
|
|
||||||
class="btn ghost"
|
|
||||||
type="button"
|
|
||||||
@click="resetForm"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="list">
|
<div class="form-actions-row">
|
||||||
<article v-for="row in items" :key="row.id" class="card">
|
<UiButton type="submit" :loading="saving">
|
||||||
<header>
|
{{ editingId ? 'ذخیره تغییرات' : 'افزودن مقاله' }}
|
||||||
<strong>{{ row.question }}</strong>
|
</UiButton>
|
||||||
<span v-if="!row.isPublished" class="tag muted-tag">پیشنویس</span>
|
<UiButton v-if="editingId" variant="ghost" type="button" @click="resetForm">
|
||||||
<span v-if="row.category" class="tag">{{ row.category }}</span>
|
انصراف
|
||||||
</header>
|
</UiButton>
|
||||||
<p>{{ row.answer }}</p>
|
</div>
|
||||||
<div class="card-actions">
|
</form>
|
||||||
<button class="btn ghost" type="button" @click="startEdit(row)">
|
</UiCard>
|
||||||
|
|
||||||
|
<UiEmptyState
|
||||||
|
v-if="!loading && !items.length"
|
||||||
|
title="هنوز مقالهای ثبت نشده است"
|
||||||
|
description="با ثبت اولین سؤال و پاسخ، پایگاه دانش سازمان را تکمیل کنید."
|
||||||
|
icon="document"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else class="articles-list">
|
||||||
|
<UiCard v-for="row in items" :key="row.id" class="article-card">
|
||||||
|
<div class="article-content">
|
||||||
|
<div class="article-title-row">
|
||||||
|
<strong class="article-question">{{ row.question }}</strong>
|
||||||
|
<div class="article-badges">
|
||||||
|
<UiBadge :variant="row.isPublished ? 'success' : 'neutral'">
|
||||||
|
{{ row.isPublished ? 'منتشرشده' : 'پیشنویس' }}
|
||||||
|
</UiBadge>
|
||||||
|
<UiBadge v-if="row.category" variant="info">
|
||||||
|
{{ row.category }}
|
||||||
|
</UiBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="article-answer">{{ row.answer }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="article-actions">
|
||||||
|
<UiButton variant="secondary" size="sm" @click="startEdit(row)">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="pencil" />
|
||||||
|
</template>
|
||||||
ویرایش
|
ویرایش
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn danger" type="button" @click="remove(row)">
|
<UiButton variant="danger" size="sm" @click="remove(row)">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="trash" />
|
||||||
|
</template>
|
||||||
حذف
|
حذف
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</UiCard>
|
||||||
<p v-if="!loading && !items.length" class="empty">هنوز مقالهای نیست.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.faq-page {
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
max-width: 720px;
|
|
||||||
min-width: 0;
|
|
||||||
overflow-x: clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-head {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: var(--sp-6);
|
||||||
align-items: flex-start;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.head-copy {
|
.editor-card {
|
||||||
min-width: 0;
|
padding: var(--sp-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.form-grid-extra {
|
||||||
margin: 0 0 0.3rem;
|
|
||||||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lead,
|
|
||||||
.muted {
|
|
||||||
margin: 0;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel,
|
|
||||||
.list {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
grid-template-columns: 1fr 1fr auto;
|
||||||
}
|
gap: var(--sp-4);
|
||||||
|
|
||||||
.panel {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
|
||||||
padding: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-fields {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-fields .field {
|
.publish-toggle-wrap {
|
||||||
flex: 1;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
textarea {
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.85rem;
|
|
||||||
font: inherit;
|
|
||||||
background: #fff;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
min-height: 5.5rem;
|
|
||||||
padding: 0.65rem 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
height: var(--control-h, 40px);
|
||||||
color: inherit;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.publish-label {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: var(--sp-2);
|
||||||
min-height: var(--control-h);
|
font-size: var(--fs-sm);
|
||||||
padding: 0 0.95rem;
|
color: var(--color-text-secondary);
|
||||||
border-radius: var(--radius);
|
|
||||||
font-weight: 700;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.form-actions-row {
|
||||||
background: #14b8a6;
|
|
||||||
color: #fff;
|
|
||||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.ghost {
|
|
||||||
background: #fff;
|
|
||||||
color: #0f766e;
|
|
||||||
border-color: #99f6e4;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.danger {
|
|
||||||
background: #fff;
|
|
||||||
color: #b91c1c;
|
|
||||||
border-color: #fecaca;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions,
|
|
||||||
.card-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: var(--sp-2);
|
||||||
flex-wrap: wrap;
|
margin-top: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.articles-list {
|
||||||
border: 1px solid #e2e8f0;
|
display: flex;
|
||||||
border-radius: 18px;
|
flex-direction: column;
|
||||||
padding: 0.9rem 1rem;
|
gap: var(--sp-4);
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card header {
|
.article-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
padding: var(--sp-4) var(--sp-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.45rem;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0.4rem;
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card p {
|
.article-question {
|
||||||
margin: 0 0 0.65rem;
|
font-size: var(--fs-md);
|
||||||
white-space: pre-wrap;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #0f766e;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted-tag {
|
.article-badges {
|
||||||
color: #64748b;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.article-answer {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #b91c1c;
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: var(--lh-relaxed);
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ok {
|
.article-actions {
|
||||||
margin: 0;
|
display: flex;
|
||||||
color: #0f766e;
|
gap: var(--sp-2);
|
||||||
font-weight: 600;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
@media (max-width: 640px) {
|
||||||
padding: 1rem;
|
.form-grid-extra {
|
||||||
border: 1px dashed #e2e8f0;
|
grid-template-columns: 1fr;
|
||||||
border-radius: 18px;
|
}
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
.article-card {
|
||||||
.row-fields {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-fields .field {
|
.article-actions {
|
||||||
min-width: 0;
|
justify-content: flex-end;
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions,
|
|
||||||
.card-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,8 +6,15 @@ import { formatMoney } from '@/lib/money';
|
|||||||
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { canManageSettings } from '@/lib/roles';
|
import { canManageSettings } from '@/lib/roles';
|
||||||
import UiButton from '@/components/ui/UiButton.vue';
|
import {
|
||||||
import UiInput from '@/components/ui/UiInput.vue';
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
UiSelect,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import type {
|
import type {
|
||||||
InventorySummaryDto,
|
InventorySummaryDto,
|
||||||
StockMovementDto,
|
StockMovementDto,
|
||||||
@@ -103,78 +110,83 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" dir="rtl">
|
<section class="crm-page">
|
||||||
<header class="head">
|
<UiPageHeader
|
||||||
<div>
|
title="انبار"
|
||||||
<h1>انبار</h1>
|
lead="موجودی کالا، ورود خرید و مانده وقتی تهحساب وصل نباشد"
|
||||||
<p class="sub">
|
>
|
||||||
موجودی کالا، ورود خرید و مانده وقتی تهحساب وصل نباشد
|
<template #actions>
|
||||||
</p>
|
<UiButton variant="ghost" type="button" :disabled="loading" @click="load">
|
||||||
</div>
|
<UiIcon name="refresh" :size="16" />
|
||||||
<UiButton variant="ghost" type="button" :disabled="loading" @click="load">
|
بروزرسانی
|
||||||
بروزرسانی
|
</UiButton>
|
||||||
</UiButton>
|
</template>
|
||||||
</header>
|
</UiPageHeader>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error-msg">{{ error }}</p>
|
||||||
<p v-else-if="loading && !summary" class="muted">در حال بارگذاری…</p>
|
<p v-else-if="loading && !summary" class="muted">در حال بارگذاری…</p>
|
||||||
|
|
||||||
<div v-else-if="summary && !summary.enabled" class="notice">
|
<div v-else-if="summary && !summary.enabled" class="notice-box">
|
||||||
اتصال تهحساب فعال است؛ موجودی و مانده از حسابداری خارجی خوانده میشود.
|
اتصال تهحساب فعال است؛ موجودی و مانده از حسابداری خارجی خوانده میشود.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else-if="summary">
|
<template v-else-if="summary">
|
||||||
<div class="kpis">
|
<div class="kpis-grid">
|
||||||
<article>
|
<UiCard padding="sm" class="kpi-card">
|
||||||
<span>صندوق (وصولها)</span>
|
<span class="kpi-label">صندوق (وصولها)</span>
|
||||||
<strong>{{ formatMoney(summary.cashTotal, summary.currency) }}</strong>
|
<strong class="kpi-value">{{ formatMoney(summary.cashTotal, summary.currency) }}</strong>
|
||||||
</article>
|
</UiCard>
|
||||||
<article>
|
<UiCard padding="sm" class="kpi-card">
|
||||||
<span>مانده طرفحسابها</span>
|
<span class="kpi-label">مانده طرفحسابها</span>
|
||||||
<strong>{{
|
<strong class="kpi-value">{{ formatMoney(summary.receivablesTotal, summary.currency) }}</strong>
|
||||||
formatMoney(summary.receivablesTotal, summary.currency)
|
</UiCard>
|
||||||
}}</strong>
|
<UiCard padding="sm" class="kpi-card">
|
||||||
</article>
|
<span class="kpi-label">نام انبار</span>
|
||||||
<article>
|
<strong class="kpi-value">{{ summary.warehouseName }}</strong>
|
||||||
<span>انبار</span>
|
</UiCard>
|
||||||
<strong>{{ summary.warehouseName }}</strong>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form v-if="canWrite" class="move-form" @submit.prevent="submitMove">
|
<UiCard v-if="canWrite" padding="sm" class="move-form-card">
|
||||||
<select v-model="moveType">
|
<form class="move-form" @submit.prevent="submitMove">
|
||||||
<option value="purchase">ورود خرید</option>
|
<UiSelect v-model="moveType">
|
||||||
<option value="adjustment">اصلاح موجودی (عدد جدید)</option>
|
<option value="purchase">ورود خرید</option>
|
||||||
</select>
|
<option value="adjustment">اصلاح موجودی (عدد جدید)</option>
|
||||||
<select v-model="productId">
|
</UiSelect>
|
||||||
<option value="" disabled>محصول</option>
|
<UiSelect v-model="productId">
|
||||||
<option
|
<option value="" disabled>انتخاب محصول</option>
|
||||||
v-for="p in summary.products"
|
<option
|
||||||
:key="p.productId"
|
v-for="p in summary.products"
|
||||||
:value="p.productId"
|
:key="p.productId"
|
||||||
>
|
:value="p.productId"
|
||||||
{{ p.name }}
|
>
|
||||||
</option>
|
{{ p.name }}
|
||||||
</select>
|
</option>
|
||||||
<UiInput
|
</UiSelect>
|
||||||
v-model="quantity"
|
<UiInput
|
||||||
type="number"
|
v-model="quantity"
|
||||||
:placeholder="moveType === 'adjustment' ? 'موجودی جدید' : 'تعداد'"
|
type="number"
|
||||||
/>
|
:placeholder="moveType === 'adjustment' ? 'موجودی جدید' : 'تعداد'"
|
||||||
<UiInput v-model="note" placeholder="توضیح (اختیاری)" />
|
/>
|
||||||
<UiButton type="submit" :disabled="saving || !productId">
|
<UiInput v-model="note" placeholder="توضیح اختیاری…" />
|
||||||
{{ saving ? 'ثبت…' : 'ثبت گردش' }}
|
<UiButton variant="primary" type="submit" :disabled="saving || !productId" :loading="saving">
|
||||||
</UiButton>
|
ثبت گردش
|
||||||
</form>
|
</UiButton>
|
||||||
|
</form>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
<div class="grid">
|
<div class="inventory-grid">
|
||||||
<div class="card">
|
<UiCard>
|
||||||
<h2>موجودی کالا</h2>
|
<template #header>
|
||||||
<p v-if="!summary.products.length" class="muted">محصولی نیست.</p>
|
<div class="section-title">
|
||||||
<table v-else>
|
<UiIcon name="box" :size="18" />
|
||||||
|
<h3>موجودی کالا</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-if="!summary.products.length" class="muted empty-note">محصولی در انبار تعریف نشده است.</p>
|
||||||
|
<table v-else class="crm-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>کالا</th>
|
<th>نام کالا</th>
|
||||||
<th>واحد</th>
|
<th>واحد</th>
|
||||||
<th>موجودی</th>
|
<th>موجودی</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -183,190 +195,185 @@ onMounted(load);
|
|||||||
<tr v-for="p in summary.products" :key="p.productId">
|
<tr v-for="p in summary.products" :key="p.productId">
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ p.name }}</strong>
|
<strong>{{ p.name }}</strong>
|
||||||
<small v-if="p.sku">{{ p.sku }}</small>
|
<small v-if="p.sku" class="sku-hint">{{ p.sku }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ p.unit || 'عدد' }}</td>
|
<td>{{ p.unit || 'عدد' }}</td>
|
||||||
<td class="num" :class="{ low: p.stockQty <= 0 }">
|
<td>
|
||||||
{{ toFaDigits(String(p.stockQty)) }}
|
<UiBadge :variant="p.stockQty > 0 ? 'success' : 'danger'" size="sm">
|
||||||
|
{{ toFaDigits(String(p.stockQty)) }}
|
||||||
|
</UiBadge>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</UiCard>
|
||||||
|
|
||||||
<div class="card">
|
<UiCard>
|
||||||
<h2>گردش اخیر</h2>
|
<template #header>
|
||||||
<p v-if="!movements.length" class="muted">گردشی ثبت نشده.</p>
|
<div class="section-title">
|
||||||
<ul v-else class="moves">
|
<UiIcon name="clock" :size="18" />
|
||||||
|
<h3>گردش اخیر کالا</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-if="!movements.length" class="muted empty-note">هیچ گردشی ثبت نشده است.</p>
|
||||||
|
<ul v-else class="moves-list">
|
||||||
<li v-for="m in movements" :key="m.id">
|
<li v-for="m in movements" :key="m.id">
|
||||||
<div>
|
<div class="move-info">
|
||||||
<strong>{{ m.productName }}</strong>
|
<strong>{{ m.productName }}</strong>
|
||||||
<span>{{ typeLabel[m.type] }}</span>
|
<span class="move-type-tag">{{ typeLabel[m.type] }}</span>
|
||||||
<small v-if="m.note">{{ m.note }}</small>
|
<small v-if="m.note" class="move-note">{{ m.note }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="move-end">
|
||||||
|
<UiBadge :variant="m.quantity > 0 ? 'success' : 'danger'" size="sm">
|
||||||
|
{{ m.quantity > 0 ? '+' : '' }}{{ toFaDigits(String(m.quantity)) }}
|
||||||
|
</UiBadge>
|
||||||
|
<time class="move-time">{{ formatJalaliNumeric(new Date(m.occurredAt)) }}</time>
|
||||||
</div>
|
</div>
|
||||||
<em :class="{ in: m.quantity > 0, out: m.quantity < 0 }">
|
|
||||||
{{ m.quantity > 0 ? '+' : '' }}{{ toFaDigits(String(m.quantity)) }}
|
|
||||||
</em>
|
|
||||||
<time>{{ formatJalaliNumeric(new Date(m.occurredAt)) }}</time>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.error-msg {
|
||||||
display: grid;
|
color: var(--color-danger);
|
||||||
gap: 1rem;
|
font-weight: 600;
|
||||||
}
|
font-size: var(--fs-sm);
|
||||||
|
|
||||||
.head {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub,
|
.notice-box {
|
||||||
.muted {
|
background: var(--color-info-surface);
|
||||||
margin: 0.25rem 0 0;
|
border: 1px solid var(--color-info);
|
||||||
color: var(--muted);
|
border-radius: var(--radius-lg);
|
||||||
font-size: 0.88rem;
|
padding: var(--sp-3) var(--sp-4);
|
||||||
|
color: var(--color-info);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.kpis-grid {
|
||||||
color: var(--danger, #dc2626);
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
display: block;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notice,
|
.kpi-value {
|
||||||
.kpis article,
|
font-size: var(--fs-lg);
|
||||||
.card,
|
color: var(--color-text-emphasis);
|
||||||
.move-form {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 1rem 1.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpis {
|
.move-form-card {
|
||||||
display: grid;
|
margin: var(--sp-2) 0;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kpis span {
|
|
||||||
display: block;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
margin-bottom: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.kpis strong {
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.move-form {
|
.move-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 10rem 1fr 8rem 1fr auto;
|
grid-template-columns: 10rem 1fr 8rem 1fr auto;
|
||||||
gap: 0.5rem;
|
gap: var(--sp-3);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.move-form select {
|
.inventory-grid {
|
||||||
min-height: var(--control-h);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 0 0.6rem;
|
|
||||||
font: inherit;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 0.75rem;
|
gap: var(--sp-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.section-title {
|
||||||
margin: 0 0 0.75rem;
|
display: flex;
|
||||||
font-size: 0.95rem;
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
.section-title h3 {
|
||||||
width: 100%;
|
margin: 0;
|
||||||
border-collapse: collapse;
|
font-size: var(--fs-base);
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
text-align: right;
|
|
||||||
padding: 0.45rem 0;
|
|
||||||
border-bottom: 1px solid #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
td small {
|
|
||||||
display: block;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.num {
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.low {
|
.sku-hint {
|
||||||
color: var(--danger, #dc2626);
|
display: block;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.moves {
|
.empty-note {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
padding: var(--sp-4) 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moves-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.65rem;
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.moves li {
|
.moves-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding: var(--sp-2) 0;
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-info {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
gap: var(--sp-1);
|
||||||
gap: 0.15rem 0.75rem;
|
|
||||||
font-size: 0.86rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.moves span,
|
.move-info strong {
|
||||||
.moves small,
|
font-size: var(--fs-sm);
|
||||||
.moves time {
|
color: var(--color-text-emphasis);
|
||||||
display: block;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.78rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.moves em {
|
.move-type-tag {
|
||||||
font-style: normal;
|
color: var(--color-text-muted);
|
||||||
font-weight: 800;
|
font-size: var(--fs-xs);
|
||||||
grid-row: span 2;
|
|
||||||
align-self: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.moves .in {
|
.move-note {
|
||||||
color: #16a34a;
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.moves .out {
|
.move-end {
|
||||||
color: #dc2626;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-time {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.kpis,
|
.kpis-grid,
|
||||||
.grid,
|
.inventory-grid,
|
||||||
.move-form {
|
.move-form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -845,12 +845,6 @@ onMounted(load);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" :class="{ embedded }">
|
<section class="page" :class="{ embedded }">
|
||||||
<div v-if="embedded" class="embed-actions no-print">
|
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="save">
|
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
|
||||||
</button>
|
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="goBack">انصراف</button>
|
|
||||||
</div>
|
|
||||||
<header v-if="!embedded" class="page-head no-print">
|
<header v-if="!embedded" class="page-head no-print">
|
||||||
<div class="head-copy">
|
<div class="head-copy">
|
||||||
<div class="title-row">
|
<div class="title-row">
|
||||||
@@ -1725,6 +1719,14 @@ onMounted(load);
|
|||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Embedded Modal Actions (Fixed at bottom) -->
|
||||||
|
<div v-if="embedded" class="embed-actions no-print">
|
||||||
|
<button class="btn primary" type="button" :disabled="saving" @click="save">
|
||||||
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn cancel" type="button" :disabled="saving" @click="goBack">انصراف</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1741,7 +1743,8 @@ onMounted(load);
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 1180px;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.embedded {
|
.page.embedded {
|
||||||
@@ -1795,7 +1798,7 @@ onMounted(load);
|
|||||||
.doc-ico {
|
.doc-ico {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: var(--teal-soft);
|
background: var(--teal-soft);
|
||||||
@@ -1806,7 +1809,7 @@ onMounted(load);
|
|||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: clamp(1.2rem, 2.4vw, 1.5rem);
|
font-size: clamp(1.2rem, 2.4vw, 1.5rem);
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1849,13 +1852,13 @@ h1 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-ico {
|
.card-ico {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: var(--teal-soft);
|
background: var(--teal-soft);
|
||||||
@@ -1877,7 +1880,7 @@ h1 {
|
|||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
height: var(--control-h);
|
height: var(--control-h);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
@@ -2017,7 +2020,7 @@ h1 {
|
|||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
padding: 0 0.85rem;
|
padding: 0 0.85rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -2064,7 +2067,7 @@ h1 {
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -2183,7 +2186,7 @@ h1 {
|
|||||||
.prod-thumb {
|
.prod-thumb {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: var(--teal-soft);
|
background: var(--teal-soft);
|
||||||
@@ -2230,7 +2233,7 @@ h1 {
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -2253,7 +2256,7 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.line-sum {
|
.line-sum {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2261,7 +2264,7 @@ h1 {
|
|||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
background: #fff1f2;
|
background: #fff1f2;
|
||||||
color: #e11d48;
|
color: #e11d48;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -2278,7 +2281,7 @@ h1 {
|
|||||||
background: var(--teal-soft);
|
background: var(--teal-soft);
|
||||||
color: var(--teal);
|
color: var(--teal);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2287,7 +2290,7 @@ h1 {
|
|||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
@@ -2311,7 +2314,7 @@ h1 {
|
|||||||
.sum-row.payable {
|
.sum-row.payable {
|
||||||
margin-top: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
padding: 0.75rem 0.85rem;
|
padding: 0.75rem 0.85rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: var(--teal-soft);
|
background: var(--teal-soft);
|
||||||
color: var(--teal);
|
color: var(--teal);
|
||||||
border: 1px solid var(--teal-border);
|
border: 1px solid var(--teal-border);
|
||||||
@@ -2382,7 +2385,7 @@ h1 {
|
|||||||
background: var(--teal-soft);
|
background: var(--teal-soft);
|
||||||
color: var(--teal);
|
color: var(--teal);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-add {
|
.icon-add {
|
||||||
@@ -2425,7 +2428,7 @@ h1 {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border: 1px solid #eef2f6;
|
border: 1px solid #eef2f6;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2448,7 +2451,7 @@ h1 {
|
|||||||
|
|
||||||
.mob-total {
|
.mob-total {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2531,7 +2534,7 @@ h1 {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.65rem 0.8rem;
|
padding: 0.65rem 0.8rem;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2606,7 +2609,7 @@ h1 {
|
|||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
|
|||||||
@@ -9,8 +9,13 @@ import {
|
|||||||
toFaDigits,
|
toFaDigits,
|
||||||
} from '@/lib/jalali';
|
} from '@/lib/jalali';
|
||||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||||
import UiButton from '@/components/ui/UiButton.vue';
|
import {
|
||||||
import UiModal from '@/components/ui/UiModal.vue';
|
UiButton,
|
||||||
|
UiModal,
|
||||||
|
UiPageHeader,
|
||||||
|
UiPagination,
|
||||||
|
UiEmptyState,
|
||||||
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import InvoiceFormView from '@/views/InvoiceFormView.vue';
|
import InvoiceFormView from '@/views/InvoiceFormView.vue';
|
||||||
import type {
|
import type {
|
||||||
@@ -41,7 +46,6 @@ type BillingRow = {
|
|||||||
href: { name: string; params: { id: string } };
|
href: { name: string; params: { id: string } };
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_SIZES = [10, 20, 50];
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -216,12 +220,6 @@ const pageItems = computed(() => {
|
|||||||
const start = (page.value - 1) * pageSize.value;
|
const start = (page.value - 1) * pageSize.value;
|
||||||
return filtered.value.slice(start, start + pageSize.value);
|
return filtered.value.slice(start, start + pageSize.value);
|
||||||
});
|
});
|
||||||
const pageFrom = computed(() =>
|
|
||||||
filtered.value.length ? (page.value - 1) * pageSize.value + 1 : 0,
|
|
||||||
);
|
|
||||||
const pageTo = computed(() =>
|
|
||||||
Math.min(page.value * pageSize.value, filtered.value.length),
|
|
||||||
);
|
|
||||||
|
|
||||||
function docLabel(doc: BillingRow['doc']) {
|
function docLabel(doc: BillingRow['doc']) {
|
||||||
if (doc === 'quote') return 'پیش فاکتور';
|
if (doc === 'quote') return 'پیش فاکتور';
|
||||||
@@ -343,35 +341,33 @@ onMounted(load);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" @click="closeMenus">
|
<section class="page" @click="closeMenus">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="title-row">
|
title="فاکتور ها"
|
||||||
<span class="title-ico"><UiIcon name="document" :size="22" /></span>
|
lead="مدیریت فاکتورهای فروش، پیش فاکتور و خدمات"
|
||||||
<div>
|
>
|
||||||
<h1>فاکتور ها</h1>
|
<template #actions>
|
||||||
<p class="lead">مدیریت فاکتورهای فروش، پیش فاکتور و خدمات</p>
|
<div class="head-actions" @click.stop>
|
||||||
</div>
|
<div class="add-split desk-only">
|
||||||
</div>
|
<UiButton type="button" @click="invoiceModal = { kind: 'invoice' }; addOpen = false">
|
||||||
<div class="head-actions" @click.stop>
|
<UiIcon name="plus" :size="16" />
|
||||||
<div class="add-split desk-only">
|
افزودن فاکتور
|
||||||
<UiButton type="button" @click="invoiceModal = { kind: 'invoice' }; addOpen = false">
|
</UiButton>
|
||||||
<UiIcon name="plus" :size="16" />
|
<button type="button" class="btn primary split" aria-label="گزینههای افزودن" @click="addOpen = !addOpen">
|
||||||
افزودن فاکتور
|
<UiIcon name="chevron" :size="14" />
|
||||||
</UiButton>
|
</button>
|
||||||
<button type="button" class="btn primary split" aria-label="گزینههای افزودن" @click="addOpen = !addOpen">
|
<div v-if="addOpen" class="menu">
|
||||||
<UiIcon name="chevron" :size="14" />
|
<button type="button" @click="invoiceModal = { kind: 'invoice' }; addOpen = false">فاکتور جدید</button>
|
||||||
</button>
|
<button type="button" @click="invoiceModal = { kind: 'quote' }; addOpen = false">پیش فاکتور جدید</button>
|
||||||
<div v-if="addOpen" class="menu">
|
<button type="button" @click="invoiceModal = { kind: 'credit' }; addOpen = false">فاکتور برگشتی</button>
|
||||||
<button type="button" @click="invoiceModal = { kind: 'invoice' }; addOpen = false">فاکتور جدید</button>
|
</div>
|
||||||
<button type="button" @click="invoiceModal = { kind: 'quote' }; addOpen = false">پیش فاکتور جدید</button>
|
|
||||||
<button type="button" @click="invoiceModal = { kind: 'credit' }; addOpen = false">فاکتور برگشتی</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<UiButton class="mob-only" type="button" @click="invoiceModal = { kind: 'invoice' }">
|
||||||
|
<UiIcon name="plus" :size="16" />
|
||||||
|
افزودن
|
||||||
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
<UiButton class="mob-only" type="button" @click="invoiceModal = { kind: 'invoice' }">
|
</template>
|
||||||
<UiIcon name="plus" :size="16" />
|
</UiPageHeader>
|
||||||
افزودن
|
|
||||||
</UiButton>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Desktop underline tabs -->
|
<!-- Desktop underline tabs -->
|
||||||
<div class="tabs desk-only">
|
<div class="tabs desk-only">
|
||||||
@@ -565,37 +561,21 @@ onMounted(load);
|
|||||||
<em :class="{ neg: r.amount < 0 }">{{ money(r.amount, r.currency) }}</em>
|
<em :class="{ neg: r.amount < 0 }">{{ money(r.amount, r.currency) }}</em>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<p v-if="!pageItems.length" class="empty">سندی یافت نشد.</p>
|
<UiEmptyState
|
||||||
|
v-if="!pageItems.length"
|
||||||
|
title="سندی یافت نشد."
|
||||||
|
description="فاکتور، پیشفاکتور یا سند جدیدی ثبت کنید."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="filtered.length" class="pager">
|
<UiPagination
|
||||||
<div class="pager-meta">
|
v-if="filtered.length"
|
||||||
<p class="muted">
|
:page="page"
|
||||||
نمایش {{ toFaDigits(pageFrom) }} تا {{ toFaDigits(pageTo) }} از
|
:total-pages="pageCount"
|
||||||
{{ toFaDigits(filtered.length) }} مورد
|
:total-items="filtered.length"
|
||||||
</p>
|
:page-size="pageSize"
|
||||||
<label class="page-size desk-only">
|
@update:page="page = $event"
|
||||||
نمایش در هر صفحه:
|
/>
|
||||||
<select v-model.number="pageSize">
|
|
||||||
<option v-for="n in PAGE_SIZES" :key="n" :value="n">{{ toFaDigits(n) }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="pages">
|
|
||||||
<button type="button" class="page-btn" :disabled="page <= 1" @click="page -= 1">‹</button>
|
|
||||||
<button
|
|
||||||
v-for="n in pageCount"
|
|
||||||
:key="n"
|
|
||||||
type="button"
|
|
||||||
class="page-btn"
|
|
||||||
:class="{ on: page === n }"
|
|
||||||
@click="page = n"
|
|
||||||
>
|
|
||||||
{{ toFaDigits(n) }}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="page-btn" :disabled="page >= pageCount" @click="page += 1">›</button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UiModal
|
<UiModal
|
||||||
@@ -682,7 +662,8 @@ onMounted(load);
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.9rem;
|
gap: 0.9rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 5rem;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.page-head {
|
.page-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -692,10 +673,10 @@ onMounted(load);
|
|||||||
}
|
}
|
||||||
.title-row { display: flex; gap: 0.75rem; align-items: flex-start; }
|
.title-row { display: flex; gap: 0.75rem; align-items: flex-start; }
|
||||||
.title-ico {
|
.title-ico {
|
||||||
width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center;
|
width: 44px; height: 44px; border-radius: var(--radius); display: grid; place-items: center;
|
||||||
background: var(--teal-soft); color: var(--teal);
|
background: var(--teal-soft); color: var(--teal);
|
||||||
}
|
}
|
||||||
h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
h1 { margin: 0; font-size: 1.5rem; font-weight: 600; }
|
||||||
.lead { margin: 0.2rem 0 0; color: var(--muted); font-size: 0.9rem; }
|
.lead { margin: 0.2rem 0 0; color: var(--muted); font-size: 0.9rem; }
|
||||||
.head-actions { position: relative; }
|
.head-actions { position: relative; }
|
||||||
.add-split { display: flex; position: relative; }
|
.add-split { display: flex; position: relative; }
|
||||||
@@ -721,11 +702,11 @@ h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
|||||||
.add-doc { color: var(--teal); }
|
.add-doc { color: var(--teal); }
|
||||||
.plus-badge {
|
.plus-badge {
|
||||||
position: absolute; bottom: 6px; left: 6px;
|
position: absolute; bottom: 6px; left: 6px;
|
||||||
background: var(--teal); color: #fff; border-radius: 4px; padding: 1px;
|
background: var(--teal); color: #fff; border-radius: var(--radius); padding: 1px;
|
||||||
}
|
}
|
||||||
.menu {
|
.menu {
|
||||||
position: absolute; top: calc(100% + 6px); left: 0; z-index: 30; min-width: 170px;
|
position: absolute; top: calc(100% + 6px); left: 0; z-index: 30; min-width: 170px;
|
||||||
background: #fff; border: 1px solid var(--line); border-radius: 12px;
|
background: #fff; border: 1px solid var(--line); border-radius: var(--radius);
|
||||||
box-shadow: 0 12px 28px rgba(15,23,42,.12); padding: 0.35rem; display: grid;
|
box-shadow: 0 12px 28px rgba(15,23,42,.12); padding: 0.35rem; display: grid;
|
||||||
}
|
}
|
||||||
.menu.left { left: auto; right: 0; }
|
.menu.left { left: auto; right: 0; }
|
||||||
@@ -733,7 +714,7 @@ h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
|||||||
.menu button {
|
.menu button {
|
||||||
display: block; width: 100%;
|
display: block; width: 100%;
|
||||||
border: 0; background: transparent; text-align: right; padding: 0.55rem 0.7rem;
|
border: 0; background: transparent; text-align: right; padding: 0.55rem 0.7rem;
|
||||||
border-radius: 8px; color: var(--text); text-decoration: none; cursor: pointer;
|
border-radius: var(--radius); color: var(--text); text-decoration: none; cursor: pointer;
|
||||||
}
|
}
|
||||||
.menu a:hover,
|
.menu a:hover,
|
||||||
.menu button:hover { background: var(--teal-soft); color: var(--teal); }
|
.menu button:hover { background: var(--teal-soft); color: var(--teal); }
|
||||||
@@ -764,21 +745,30 @@ h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
|||||||
|
|
||||||
.toolbar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
.toolbar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
||||||
.search {
|
.search {
|
||||||
flex: 1 1 220px; display: flex; align-items: center; gap: 0.45rem;
|
flex: 1 1 220px; max-width: 380px; display: flex; align-items: center; gap: 0.45rem;
|
||||||
border: 1px solid var(--line); border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h);
|
border: 1px solid var(--line); border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h);
|
||||||
padding: 0 0.75rem; background: #fff; color: var(--muted); box-sizing: border-box;
|
padding: 0 0.75rem; background: #fff; color: var(--muted); box-sizing: border-box;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
}
|
}
|
||||||
.search input { border: 0; outline: 0; width: 100%; background: transparent; color: var(--text); }
|
.search:focus-within {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||||
|
}
|
||||||
|
.search input { border: 0; outline: 0; width: 100%; background: transparent; color: var(--ink); font-size: var(--fs-sm); padding: 0; }
|
||||||
.search .mob-ph { display: none; }
|
.search .mob-ph { display: none; }
|
||||||
.filters { display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center; }
|
.filters { display: flex; flex-wrap: wrap; gap: 0.45rem; align-items: center; }
|
||||||
.filter-chip {
|
.filter-chip {
|
||||||
display: inline-flex; align-items: center; gap: 0.3rem; flex-wrap: nowrap;
|
display: inline-flex; align-items: center; gap: 0.3rem; flex-wrap: nowrap;
|
||||||
border: 1px solid var(--line); border-radius: var(--radius); padding: 0 0.55rem;
|
border: 1px solid var(--line); border-radius: var(--radius); padding: 0 0.55rem;
|
||||||
background: #fff; color: #94a3b8; min-height: var(--control-h); height: var(--control-h);
|
background: #fff; color: var(--muted); min-height: var(--control-h); height: var(--control-h);
|
||||||
box-sizing: border-box; white-space: nowrap;
|
box-sizing: border-box; white-space: nowrap; font-size: var(--fs-sm);
|
||||||
|
transition: border-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
.filter-chip:focus-within {
|
||||||
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
.filter-chip select, .filter-chip :deep(button), .filter-chip :deep(input) {
|
.filter-chip select, .filter-chip :deep(button), .filter-chip :deep(input) {
|
||||||
border: 0; background: transparent; outline: 0; min-height: 0; height: 32px; color: var(--text);
|
border: 0; background: transparent; outline: 0; min-height: 0; height: 32px; color: var(--ink); font-size: var(--fs-sm);
|
||||||
}
|
}
|
||||||
.filter-chip :deep(.wrap) { width: auto; flex: 0 1 auto; }
|
.filter-chip :deep(.wrap) { width: auto; flex: 0 1 auto; }
|
||||||
.filter-chip :deep(.trigger) {
|
.filter-chip :deep(.trigger) {
|
||||||
@@ -790,7 +780,7 @@ h1 { margin: 0; font-size: 1.5rem; font-weight: 800; }
|
|||||||
.filter-chip .sep { color: #cbd5e1; flex-shrink: 0; }
|
.filter-chip .sep { color: #cbd5e1; flex-shrink: 0; }
|
||||||
|
|
||||||
.table-wrap {
|
.table-wrap {
|
||||||
background: #fff; border: 1px solid #eef2f7; border-radius: 16px; overflow: auto;
|
background: #fff; border: 1px solid #eef2f7; border-radius: var(--radius); overflow: auto;
|
||||||
box-shadow: 0 8px 24px rgba(15,23,42,.04);
|
box-shadow: 0 8px 24px rgba(15,23,42,.04);
|
||||||
}
|
}
|
||||||
table { width: 100%; border-collapse: collapse; min-width: 980px; }
|
table { width: 100%; border-collapse: collapse; min-width: 980px; }
|
||||||
@@ -802,7 +792,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
|||||||
.cust { display: flex; gap: 0.55rem; align-items: center; }
|
.cust { display: flex; gap: 0.55rem; align-items: center; }
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 36px; height: 36px; border-radius: 999px; background: var(--teal-soft); color: var(--teal);
|
width: 36px; height: 36px; border-radius: 999px; background: var(--teal-soft); color: var(--teal);
|
||||||
display: grid; place-items: center; font-weight: 800; flex-shrink: 0;
|
display: grid; place-items: center; font-weight: 600; flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.cust strong { display: block; }
|
.cust strong { display: block; }
|
||||||
.cust small { color: var(--muted); }
|
.cust small { color: var(--muted); }
|
||||||
@@ -832,7 +822,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
|||||||
.mob-list { display: none; gap: 0.65rem; }
|
.mob-list { display: none; gap: 0.65rem; }
|
||||||
.mob-card {
|
.mob-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #fff; border: 1px solid #eef2f7; border-radius: 16px; padding: 0.9rem;
|
background: #fff; border: 1px solid #eef2f7; border-radius: var(--radius); padding: 0.9rem;
|
||||||
box-shadow: 0 6px 16px rgba(15,23,42,.04);
|
box-shadow: 0 6px 16px rgba(15,23,42,.04);
|
||||||
display: grid; gap: 0.65rem;
|
display: grid; gap: 0.65rem;
|
||||||
}
|
}
|
||||||
@@ -847,7 +837,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
|||||||
.mob-aside { display: grid; justify-items: center; gap: 0.35rem; }
|
.mob-aside { display: grid; justify-items: center; gap: 0.35rem; }
|
||||||
.mob-menu { top: 2.2rem; }
|
.mob-menu { top: 2.2rem; }
|
||||||
.mob-card-bottom { display: flex; justify-content: flex-start; }
|
.mob-card-bottom { display: flex; justify-content: flex-start; }
|
||||||
.mob-card-bottom em { font-style: normal; font-weight: 800; font-size: 0.92rem; }
|
.mob-card-bottom em { font-style: normal; font-weight: 600; font-size: 0.92rem; }
|
||||||
|
|
||||||
.pager {
|
.pager {
|
||||||
display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem; align-items: center;
|
display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem; align-items: center;
|
||||||
@@ -857,11 +847,11 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
|||||||
display: inline-flex; align-items: center; gap: 0.35rem; color: var(--muted); font-size: 0.88rem;
|
display: inline-flex; align-items: center; gap: 0.35rem; color: var(--muted); font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
.page-size select {
|
.page-size select {
|
||||||
border: 1px solid var(--line); border-radius: 10px; padding: 0.3rem 0.5rem; background: #fff;
|
border: 1px solid var(--line); border-radius: var(--radius); padding: 0.3rem 0.5rem; background: #fff;
|
||||||
}
|
}
|
||||||
.pages { display: flex; gap: 0.3rem; }
|
.pages { display: flex; gap: 0.3rem; }
|
||||||
.page-btn {
|
.page-btn {
|
||||||
min-width: 34px; height: 34px; border-radius: 10px; border: 1px solid var(--line);
|
min-width: 34px; height: 34px; border-radius: var(--radius); border: 1px solid var(--line);
|
||||||
background: #fff; cursor: pointer;
|
background: #fff; cursor: pointer;
|
||||||
}
|
}
|
||||||
.page-btn.on { background: var(--teal); color: #fff; border-color: transparent; }
|
.page-btn.on { background: var(--teal); color: #fff; border-color: transparent; }
|
||||||
@@ -869,7 +859,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
|||||||
.fab {
|
.fab {
|
||||||
display: none; position: fixed; left: 1rem; right: 1rem; bottom: 1rem; z-index: 40;
|
display: none; position: fixed; left: 1rem; right: 1rem; bottom: 1rem; z-index: 40;
|
||||||
background: var(--teal); color: #fff; text-align: center; text-decoration: none;
|
background: var(--teal); color: #fff; text-align: center; text-decoration: none;
|
||||||
border-radius: 14px; padding: 0.9rem; font-weight: 800;
|
border-radius: var(--radius); padding: 0.9rem; font-weight: 600;
|
||||||
box-shadow: 0 12px 28px rgba(13,148,136,.35);
|
box-shadow: 0 12px 28px rgba(13,148,136,.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,12 +868,12 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
|||||||
display: grid; align-items: end;
|
display: grid; align-items: end;
|
||||||
}
|
}
|
||||||
.sheet {
|
.sheet {
|
||||||
background: #fff; border-radius: 20px 20px 0 0; padding: 1rem; max-height: 85vh; overflow: auto;
|
background: #fff; border-radius: var(--radius, 10px) var(--radius, 10px) 0 0; padding: 16px; max-height: 85vh; overflow: auto;
|
||||||
}
|
}
|
||||||
.sheet header {
|
.sheet header {
|
||||||
display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem;
|
display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
.sheet header strong { justify-self: end; }
|
.sheet header strong { justify-self: end; font-size: 16px; font-weight: 500; color: var(--ink); }
|
||||||
.link { border: 0; background: transparent; color: var(--teal); font-weight: 700; cursor: pointer; }
|
.link { border: 0; background: transparent; color: var(--teal); font-weight: 700; cursor: pointer; }
|
||||||
.lbl { margin: 0.85rem 0 0.4rem; font-weight: 700; color: #334155; }
|
.lbl { margin: 0.85rem 0 0.4rem; font-weight: 700; color: #334155; }
|
||||||
.pill-row { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
.pill-row { display: flex; flex-wrap: wrap; gap: 0.4rem; }
|
||||||
@@ -894,7 +884,7 @@ tbody tr:nth-child(even) { background: #fcfdff; }
|
|||||||
.pill-row button.on { background: var(--teal); color: #fff; border-color: transparent; }
|
.pill-row button.on { background: var(--teal); color: #fff; border-color: transparent; }
|
||||||
.field { display: grid; gap: 0.3rem; color: var(--muted); font-size: 0.85rem; margin-top: 0.35rem; }
|
.field { display: grid; gap: 0.3rem; color: var(--muted); font-size: 0.85rem; margin-top: 0.35rem; }
|
||||||
.field input, .field select, .control select {
|
.field input, .field select, .control select {
|
||||||
border: 1px solid var(--line); border-radius: 12px; padding: 0.55rem 0.7rem; background: #fff; color: var(--text);
|
border: 1px solid var(--line); border-radius: var(--radius); padding: 0.55rem 0.7rem; background: #fff; color: var(--text);
|
||||||
}
|
}
|
||||||
.control { position: relative; display: flex; align-items: center; }
|
.control { position: relative; display: flex; align-items: center; }
|
||||||
.control :deep(.ico) {
|
.control :deep(.ico) {
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ onMounted(load);
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.page { --teal: #14b8a6; display: grid; gap: 0.85rem; }
|
.page { --teal: #14b8a6; display: grid; gap: 0.85rem; }
|
||||||
.top { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; }
|
.top { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; }
|
||||||
h1 { margin: 0; font-size: 1.55rem; font-weight: 800; display: inline-flex; gap: 0.35rem; align-items: center; }
|
h1 { margin: 0; font-size: 1.55rem; font-weight: 600; display: inline-flex; gap: 0.35rem; align-items: center; }
|
||||||
h1 :deep(.ico) { color: var(--teal); }
|
h1 :deep(.ico) { color: var(--teal); }
|
||||||
.top p, .back { color: #64748b; }
|
.top p, .back { color: #64748b; }
|
||||||
.back { background: none; border: 0; font: inherit; cursor: pointer; padding: 0; }
|
.back { background: none; border: 0; font: inherit; cursor: pointer; padding: 0; }
|
||||||
@@ -565,31 +565,31 @@ h1 :deep(.ico) { color: var(--teal); }
|
|||||||
.btn { display: inline-flex; align-items: center; gap: 0.3rem; min-height: var(--control-h); height: var(--control-h); padding: 0 0.9rem; border-radius: var(--radius); font: inherit; font-weight: 700; border: 1px solid transparent; cursor: pointer; text-decoration: none; box-sizing: border-box; }
|
.btn { display: inline-flex; align-items: center; gap: 0.3rem; min-height: var(--control-h); height: var(--control-h); padding: 0 0.9rem; border-radius: var(--radius); font: inherit; font-weight: 700; border: 1px solid transparent; cursor: pointer; text-decoration: none; box-sizing: border-box; }
|
||||||
.btn.primary { background: var(--teal); color: #fff; }
|
.btn.primary { background: var(--teal); color: #fff; }
|
||||||
.btn.ghost { background: #fff; color: #0f766e; border-color: #99f6e4; }
|
.btn.ghost { background: #fff; color: #0f766e; border-color: #99f6e4; }
|
||||||
.banner { margin: 0; padding: 0.6rem 0.8rem; border-radius: 12px; }
|
.banner { margin: 0; padding: 0.6rem 0.8rem; border-radius: var(--radius); }
|
||||||
.banner.err { background: #fef2f2; color: #b91c1c; }
|
.banner.err { background: #fef2f2; color: #b91c1c; }
|
||||||
.banner.ok { background: #ecfdf5; color: #047857; }
|
.banner.ok { background: #ecfdf5; color: #047857; }
|
||||||
.tabs { display: flex; gap: 0.35rem; flex-wrap: wrap; background: #fff; padding: 0.35rem; border-radius: 14px; }
|
.tabs { display: flex; gap: 0.35rem; flex-wrap: wrap; background: #fff; padding: 0.35rem; border-radius: var(--radius); }
|
||||||
.tabs button { border: 0; background: transparent; min-height: 38px; padding: 0 0.85rem; border-radius: 10px; font: inherit; font-weight: 700; color: #0f766e; cursor: pointer; display: inline-flex; align-items: center; gap: 0.3rem; }
|
.tabs button { border: 0; background: transparent; min-height: 38px; padding: 0 0.85rem; border-radius: var(--radius); font: inherit; font-weight: 700; color: #0f766e; cursor: pointer; display: inline-flex; align-items: center; gap: 0.3rem; }
|
||||||
.tabs button.on { background: var(--teal); color: #fff; }
|
.tabs button.on { background: var(--teal); color: #fff; }
|
||||||
.workspace { display: grid; grid-template-columns: 280px minmax(0,1fr) 240px; gap: 0.85rem; align-items: start; }
|
.workspace { display: grid; grid-template-columns: 280px minmax(0,1fr) 240px; gap: 0.85rem; align-items: start; }
|
||||||
.preview-col, .canvas, .toolbox, .panel { background: #fff; border-radius: 16px; padding: 0.9rem; box-shadow: 0 8px 22px rgba(15,23,42,.04); }
|
.preview-col, .canvas, .toolbox, .panel { background: #fff; border-radius: var(--radius); padding: 0.9rem; box-shadow: 0 8px 22px rgba(15,23,42,.04); }
|
||||||
.device-toggle { display: flex; gap: 0.3rem; margin-bottom: 0.7rem; }
|
.device-toggle { display: flex; gap: 0.3rem; margin-bottom: 0.7rem; }
|
||||||
.device-toggle button { flex: 1; border: 0; min-height: 34px; border-radius: 10px; font: inherit; font-weight: 700; background: #f1f5f9; cursor: pointer; }
|
.device-toggle button { flex: 1; border: 0; min-height: 34px; border-radius: var(--radius); font: inherit; font-weight: 700; background: #f1f5f9; cursor: pointer; }
|
||||||
.device-toggle button.on { background: var(--teal); color: #fff; }
|
.device-toggle button.on { background: var(--teal); color: #fff; }
|
||||||
.phone { border: 8px solid #0f172a; border-radius: 28px; overflow: hidden; background: #fff; max-width: 260px; margin: 0 auto; }
|
.phone { border: 8px solid #0f172a; border-radius: var(--radius); overflow: hidden; background: #fff; max-width: 260px; margin: 0 auto; }
|
||||||
.desk-overlay { padding: 1.2rem; }
|
.desk-overlay { padding: 1.2rem; }
|
||||||
.desk-sheet {
|
.desk-sheet {
|
||||||
width: min(920px, 100%);
|
width: min(920px, 100%);
|
||||||
max-height: calc(100vh - 2.4rem);
|
max-height: calc(100vh - 2.4rem);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #eef2f6;
|
background: #eef2f6;
|
||||||
border-radius: 20px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem 1.1rem 1.2rem;
|
padding: 1rem 1.1rem 1.2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
.desk-sheet header { display: flex; justify-content: space-between; align-items: center; }
|
.desk-sheet header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
.desk-form { background: #fff; border-radius: 18px; overflow: hidden; box-shadow: 0 16px 40px rgba(15,23,42,.08); }
|
.desk-form { background: #fff; border-radius: var(--radius); overflow: hidden; box-shadow: 0 16px 40px rgba(15,23,42,.08); }
|
||||||
.desk-form .cover { height: 140px; }
|
.desk-form .cover { height: 140px; }
|
||||||
.desk-form .cover img { width: 100%; height: 140px; object-fit: cover; }
|
.desk-form .cover img { width: 100%; height: 140px; object-fit: cover; }
|
||||||
.desk-body { padding: 1.2rem 1.4rem 1.4rem; }
|
.desk-body { padding: 1.2rem 1.4rem 1.4rem; }
|
||||||
@@ -603,26 +603,26 @@ h1 :deep(.ico) { color: var(--teal); }
|
|||||||
.preview-body h3 { margin: 0; font-size: 1rem; }
|
.preview-body h3 { margin: 0; font-size: 1rem; }
|
||||||
.preview-body p, .preview-body small { margin: 0; color: #64748b; font-size: 0.78rem; }
|
.preview-body p, .preview-body small { margin: 0; color: #64748b; font-size: 0.78rem; }
|
||||||
.preview-body label { display: grid; gap: 0.25rem; font-size: 0.78rem; color: #475569; }
|
.preview-body label { display: grid; gap: 0.25rem; font-size: 0.78rem; color: #475569; }
|
||||||
.preview-body input, .preview-body textarea, .preview-body select { border: 1px solid #e2e8f0; border-radius: 10px; padding: 0.45rem 0.6rem; font: inherit; background: #f8fafc; }
|
.preview-body input, .preview-body textarea, .preview-body select { border: 1px solid #e2e8f0; border-radius: var(--radius); padding: 0.45rem 0.6rem; font: inherit; background: #f8fafc; }
|
||||||
.send { border: 0; color: #fff; border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h); font: inherit; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; gap: 0.3rem; box-sizing: border-box; }
|
.send { border: 0; color: #fff; border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h); font: inherit; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; gap: 0.3rem; box-sizing: border-box; }
|
||||||
.opts, .stars { color: #64748b; font-size: 0.75rem; display: grid; gap: 0.2rem; }
|
.opts, .stars { color: #64748b; font-size: 0.75rem; display: grid; gap: 0.2rem; }
|
||||||
.cover-box { width: 100%; min-height: 88px; border: 1.5px dashed #99f6e4; border-radius: 14px; background: #f0fdfa; color: #0f766e; font: inherit; font-weight: 700; cursor: pointer; overflow: hidden; }
|
.cover-box { width: 100%; min-height: 88px; border: 1.5px dashed #99f6e4; border-radius: var(--radius); background: #f0fdfa; color: #0f766e; font: inherit; font-weight: 700; cursor: pointer; overflow: hidden; }
|
||||||
.cover-box img { width: 100%; height: 88px; object-fit: cover; display: block; }
|
.cover-box img { width: 100%; height: 88px; object-fit: cover; display: block; }
|
||||||
.title-card, .field-card, .submit-card, .adv-box { border: 1px solid #eef2f7; border-radius: 14px; padding: 0.8rem; display: grid; gap: 0.45rem; }
|
.title-card, .field-card, .submit-card, .adv-box { border: 1px solid #eef2f7; border-radius: var(--radius); padding: 0.8rem; display: grid; gap: 0.45rem; }
|
||||||
.title { font-size: 1.15rem; font-weight: 800; border: 0; background: transparent; font: inherit; }
|
.title { font-size: 1.15rem; font-weight: 600; border: 0; background: transparent; font: inherit; }
|
||||||
.title-card textarea, .submit-card input, .panel input, .panel select, .sheet input, .sheet select, .sheet textarea {
|
.title-card textarea, .submit-card input, .panel input, .panel select, .sheet input, .sheet select, .sheet textarea {
|
||||||
border: 1px solid #e2e8f0; border-radius: 10px; padding: 0.55rem 0.7rem; font: inherit; width: 100%;
|
border: 1px solid #e2e8f0; border-radius: var(--radius); padding: 0.55rem 0.7rem; font: inherit; width: 100%;
|
||||||
}
|
}
|
||||||
.field-card { grid-template-columns: auto 1fr auto; align-items: center; cursor: grab; }
|
.field-card { grid-template-columns: auto 1fr auto; align-items: center; cursor: grab; }
|
||||||
.field-card small { color: #94a3b8; display: block; }
|
.field-card small { color: #94a3b8; display: block; }
|
||||||
.fops { display: flex; gap: 0.2rem; }
|
.fops { display: flex; gap: 0.2rem; }
|
||||||
.fops button { width: 30px; height: 30px; border: 0; border-radius: 8px; background: #f8fafc; cursor: pointer; color: #64748b; }
|
.fops button { width: 30px; height: 30px; border: 0; border-radius: var(--radius); background: #f8fafc; cursor: pointer; color: #64748b; }
|
||||||
.fops .danger { color: #dc2626; }
|
.fops .danger { color: #dc2626; }
|
||||||
.add-field { width: 100%; min-height: var(--control-h); height: var(--control-h); border: 1.5px dashed #14b8a6; background: #f0fdfa; color: #0f766e; border-radius: var(--radius); font: inherit; font-weight: 700; cursor: pointer; box-sizing: border-box; }
|
.add-field { width: 100%; min-height: var(--control-h); height: var(--control-h); border: 1.5px dashed #14b8a6; background: #f0fdfa; color: #0f766e; border-radius: var(--radius); font: inherit; font-weight: 700; cursor: pointer; box-sizing: border-box; }
|
||||||
.adv { background: none; border: 0; font: inherit; font-weight: 700; color: #0f766e; cursor: pointer; text-align: right; }
|
.adv { background: none; border: 0; font: inherit; font-weight: 700; color: #0f766e; cursor: pointer; text-align: right; }
|
||||||
.toolbox h3 { margin: 0 0 0.5rem; font-size: 0.95rem; }
|
.toolbox h3 { margin: 0 0 0.5rem; font-size: 0.95rem; }
|
||||||
.tools { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; margin-bottom: 1rem; }
|
.tools { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; margin-bottom: 1rem; }
|
||||||
.tools button, .ready div button { border: 1px solid #eef2f7; background: #f8fafc; border-radius: 10px; min-height: 52px; font: inherit; font-size: 0.75rem; cursor: pointer; display: grid; place-items: center; gap: 0.15rem; color: #334155; }
|
.tools button, .ready div button { border: 1px solid #eef2f7; background: #f8fafc; border-radius: var(--radius); min-height: 52px; font: inherit; font-size: 0.75rem; cursor: pointer; display: grid; place-items: center; gap: 0.15rem; color: #334155; }
|
||||||
.ready { border-bottom: 1px solid #f1f5f9; }
|
.ready { border-bottom: 1px solid #f1f5f9; }
|
||||||
.ready > button { width: 100%; background: none; border: 0; text-align: right; font: inherit; font-weight: 700; padding: 0.55rem 0; cursor: pointer; }
|
.ready > button { width: 100%; background: none; border: 0; text-align: right; font: inherit; font-weight: 700; padding: 0.55rem 0; cursor: pointer; }
|
||||||
.panel { display: grid; gap: 0.7rem; }
|
.panel { display: grid; gap: 0.7rem; }
|
||||||
@@ -635,7 +635,7 @@ h1 :deep(.ico) { color: var(--teal); }
|
|||||||
.row input { flex: 1; }
|
.row input { flex: 1; }
|
||||||
.sub { border-bottom: 1px solid #f1f5f9; padding: 0.5rem 0; }
|
.sub { border-bottom: 1px solid #f1f5f9; padding: 0.5rem 0; }
|
||||||
.overlay { position: fixed; inset: 0; background: rgba(15,23,42,.35); display: grid; place-items: center; z-index: 40; padding: 1rem; }
|
.overlay { position: fixed; inset: 0; background: rgba(15,23,42,.35); display: grid; place-items: center; z-index: 40; padding: 1rem; }
|
||||||
.sheet { width: min(420px,100%); background: #fff; border-radius: 16px; padding: 1rem; display: grid; gap: 0.65rem; }
|
.sheet { width: min(420px,100%); background: #fff; border-radius: var(--radius); padding: 1rem; display: grid; gap: 0.65rem; }
|
||||||
.link { background: none; border: 0; color: #0f766e; font: inherit; font-weight: 700; cursor: pointer; }
|
.link { background: none; border: 0; color: #0f766e; font: inherit; font-weight: 700; cursor: pointer; }
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.workspace { grid-template-columns: 1fr; }
|
.workspace { grid-template-columns: 1fr; }
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ onMounted(load);
|
|||||||
.panel {
|
.panel {
|
||||||
width: min(440px, 100%);
|
width: min(440px, 100%);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 22px;
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { computed, onMounted, ref } from 'vue';
|
|||||||
import { RouterLink, useRouter } from 'vue-router';
|
import { RouterLink, useRouter } from 'vue-router';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
|
import {
|
||||||
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiEmptyState,
|
||||||
|
UiPageHeader,
|
||||||
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import type { LeadFormDto } from '@mento/shared';
|
import type { LeadFormDto } from '@mento/shared';
|
||||||
import { toFaDigits } from '@/lib/jalali';
|
import { toFaDigits } from '@/lib/jalali';
|
||||||
@@ -59,59 +66,186 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="crm-page lead-forms-page" dir="rtl">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div>
|
title="فرمساز وب"
|
||||||
<h1>
|
description="فرمهای آنلاین بسازید، فیلدها را بچینید و لیدها را به صورت خودکار در کاریز و باشگاه مشتریان دریافت کنید."
|
||||||
فرمساز
|
>
|
||||||
<UiIcon name="layers" :size="22" />
|
<template #actions>
|
||||||
</h1>
|
<UiButton :loading="saving" @click="createForm">
|
||||||
<p class="lead">فرم وب بسازید، فیلدها را بچینید و پاسخها را در باشگاه مشتریان و کاریز ببینید.</p>
|
<template #icon>
|
||||||
</div>
|
<UiIcon name="plus" />
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="createForm">
|
</template>
|
||||||
+ فرم جدید
|
فرم جدید
|
||||||
</button>
|
</UiButton>
|
||||||
</header>
|
</template>
|
||||||
<p v-if="error" class="err">{{ error }}</p>
|
</UiPageHeader>
|
||||||
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
|
||||||
<p v-else class="muted">{{ countLabel }} فرم</p>
|
|
||||||
|
|
||||||
<div class="list">
|
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||||||
<article v-for="form in forms" :key="form.id" class="card">
|
|
||||||
<div>
|
<div v-else-if="!loading && forms.length" class="forms-summary">
|
||||||
<strong>{{ form.name }}</strong>
|
<span class="forms-count-badge">{{ countLabel }} فرم ساخته شده</span>
|
||||||
<p>{{ form.isActive ? 'فعال' : 'غیرفعال' }} · {{ toFaDigits(form.fields.length) }} فیلد · {{ toFaDigits(form.submissionCount ?? 0) }} پاسخ</p>
|
</div>
|
||||||
|
|
||||||
|
<UiEmptyState
|
||||||
|
v-if="!loading && !forms.length"
|
||||||
|
title="هنوز فرمی نساختهاید"
|
||||||
|
description="با ساخت اولین فرم، میتوانید در وبسایت یا شبکههای اجتماعی سرنخ و مشتری جذب کنید."
|
||||||
|
icon="document"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<UiButton :loading="saving" @click="createForm">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="plus" />
|
||||||
|
</template>
|
||||||
|
ساخت اولین فرم
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiEmptyState>
|
||||||
|
|
||||||
|
<div v-else class="forms-list">
|
||||||
|
<UiCard v-for="form in forms" :key="form.id" class="form-card">
|
||||||
|
<div class="form-info">
|
||||||
|
<div class="form-title-row">
|
||||||
|
<strong class="form-name">{{ form.name }}</strong>
|
||||||
|
<UiBadge :variant="form.isActive ? 'success' : 'neutral'">
|
||||||
|
{{ form.isActive ? 'فعال' : 'غیرفعال' }}
|
||||||
|
</UiBadge>
|
||||||
|
</div>
|
||||||
|
<p class="form-meta">
|
||||||
|
<span>{{ toFaDigits(form.fields.length) }} فیلد سفارشی</span>
|
||||||
|
<span class="meta-dot">·</span>
|
||||||
|
<span>{{ toFaDigits(form.submissionCount ?? 0) }} ارسال شده</span>
|
||||||
|
<span v-if="form.source" class="meta-dot">·</span>
|
||||||
|
<span v-if="form.source">منبع: {{ form.source }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ops">
|
|
||||||
<RouterLink class="btn ghost" :to="{ name: 'lead-form-builder', params: { id: form.id } }">ویرایش</RouterLink>
|
<div class="form-actions">
|
||||||
<a class="btn ghost" :href="form.publicPath" target="_blank" rel="noopener">پیشنمایش</a>
|
<RouterLink :to="{ name: 'lead-form-builder', params: { id: form.id } }">
|
||||||
<button class="link danger" type="button" @click="removeForm(form)">حذف</button>
|
<UiButton variant="secondary" size="sm">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="pencil" />
|
||||||
|
</template>
|
||||||
|
ویرایش فیلدها
|
||||||
|
</UiButton>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<a :href="form.publicPath" target="_blank" rel="noopener" class="preview-link">
|
||||||
|
<UiButton variant="ghost" size="sm">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="eye" />
|
||||||
|
</template>
|
||||||
|
پیشنمایش
|
||||||
|
</UiButton>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<UiButton variant="danger" size="sm" @click="removeForm(form)">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="trash" />
|
||||||
|
</template>
|
||||||
|
حذف
|
||||||
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</UiCard>
|
||||||
<p v-if="!loading && !forms.length" class="empty">هنوز فرمی نساختهاید.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page { display: grid; gap: 1rem; }
|
.lead-forms-page {
|
||||||
.page-head { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; }
|
display: flex;
|
||||||
h1 { margin: 0; font-size: clamp(1.45rem, 2.4vw, 1.85rem); font-weight: 800; display: inline-flex; gap: 0.4rem; align-items: center; }
|
flex-direction: column;
|
||||||
h1 :deep(.ico) { color: #14b8a6; }
|
gap: var(--sp-6);
|
||||||
.lead, .muted { margin: 0.25rem 0 0; color: #64748b; }
|
width: 100%;
|
||||||
.err { color: #b91c1c; }
|
max-width: 100%;
|
||||||
.btn, .link { font: inherit; cursor: pointer; }
|
}
|
||||||
.btn { display: inline-flex; align-items: center; justify-content: center; min-height: var(--control-h); height: var(--control-h); padding: 0 0.95rem; border-radius: var(--radius); font-weight: 700; border: 1px solid transparent; text-decoration: none; box-sizing: border-box; }
|
|
||||||
.btn.primary { background: #14b8a6; color: #fff; }
|
.forms-summary {
|
||||||
.btn.ghost { background: #fff; color: #0f766e; border-color: #99f6e4; }
|
display: flex;
|
||||||
.link { background: none; border: 0; font-weight: 700; }
|
align-items: center;
|
||||||
.danger { color: #b91c1c; }
|
}
|
||||||
.list { display: grid; gap: 0.75rem; }
|
|
||||||
.card { background: #fff; border-radius: 16px; padding: 1rem 1.05rem; box-shadow: 0 8px 22px rgba(15,23,42,.04); display: flex; justify-content: space-between; gap: 1rem; align-items: center; }
|
.forms-count-badge {
|
||||||
.card p { margin: 0.25rem 0 0; color: #64748b; font-size: 0.88rem; }
|
font-size: var(--fs-sm);
|
||||||
.ops { display: flex; gap: 0.45rem; flex-wrap: wrap; align-items: center; }
|
color: var(--color-text-secondary);
|
||||||
.empty { padding: 1rem; border: 1px dashed #e2e8f0; border-radius: 16px; color: #64748b; text-align: center; }
|
background: var(--color-surface-soft);
|
||||||
@media (max-width: 700px) {
|
padding: var(--sp-1) var(--sp-3);
|
||||||
.page-head, .card, .ops { flex-direction: column; align-items: stretch; }
|
border-radius: var(--radius-full);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.forms-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
padding: var(--sp-4) var(--sp-5);
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card:hover {
|
||||||
|
border-color: var(--color-brand-300, #5eead4);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-name {
|
||||||
|
font-size: var(--fs-md);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-meta {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-dot {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.form-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ function cancelOrgChoice() {
|
|||||||
display: grid;
|
display: grid;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 28px;
|
border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: min(640px, calc(100dvh - 5.5rem));
|
min-height: min(640px, calc(100dvh - 5.5rem));
|
||||||
@@ -374,7 +374,7 @@ function cancelOrgChoice() {
|
|||||||
|
|
||||||
.mobile-brand strong {
|
.mobile-brand strong {
|
||||||
font-size: 1.35rem;
|
font-size: 1.35rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-brand span {
|
.mobile-brand span {
|
||||||
@@ -408,14 +408,14 @@ h1 {
|
|||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
margin-bottom: 1.1rem;
|
margin-bottom: 1.1rem;
|
||||||
padding: 0.3rem;
|
padding: 0.3rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
background: color-mix(in srgb, var(--line) 55%, white);
|
background: color-mix(in srgb, var(--line) 55%, white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-tabs button {
|
.mode-tabs button {
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 11px;
|
border-radius: var(--radius);
|
||||||
padding: 0.65rem 0.5rem;
|
padding: 0.65rem 0.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -440,7 +440,7 @@ h1 {
|
|||||||
|
|
||||||
.input-wrap input {
|
.input-wrap input {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
padding: 0.85rem 2.6rem 0.85rem 0.95rem;
|
padding: 0.85rem 2.6rem 0.85rem 0.95rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -518,7 +518,7 @@ h1 {
|
|||||||
.submit {
|
.submit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.9rem 1.1rem;
|
padding: 0.9rem 1.1rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,7 +553,7 @@ h1 {
|
|||||||
.dev-hint {
|
.dev-hint {
|
||||||
margin: -0.4rem 0 1rem;
|
margin: -0.4rem 0 1rem;
|
||||||
padding: 0.65rem 0.8rem;
|
padding: 0.65rem 0.8rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
|
|||||||
@@ -1033,7 +1033,7 @@ onBeforeUnmount(() => {
|
|||||||
h1 {
|
h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: clamp(1.45rem, 2.4vw, 1.85rem);
|
font-size: clamp(1.45rem, 2.4vw, 1.85rem);
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1043,7 +1043,7 @@ h1 {
|
|||||||
.crown :deep(.ico) { fill: #eab308; stroke: #ca8a04; }
|
.crown :deep(.ico) { fill: #eab308; stroke: #ca8a04; }
|
||||||
.lead, .sub, .muted { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.9rem; }
|
.lead, .sub, .muted { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.9rem; }
|
||||||
h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
||||||
.banner { margin: 0; padding: 0.65rem 0.85rem; border-radius: 12px; font-weight: 600; }
|
.banner { margin: 0; padding: 0.65rem 0.85rem; border-radius: var(--radius); font-weight: 600; }
|
||||||
.banner.err { color: #b91c1c; background: #fef2f2; }
|
.banner.err { color: #b91c1c; background: #fef2f2; }
|
||||||
.banner.ok { color: #047857; background: #ecfdf5; }
|
.banner.ok { color: #047857; background: #ecfdf5; }
|
||||||
.banner.warn { color: #b45309; background: #fffbeb; }
|
.banner.warn { color: #b45309; background: #fffbeb; }
|
||||||
@@ -1055,20 +1055,20 @@ h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
|||||||
}
|
}
|
||||||
.kpis.mobile { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5rem; }
|
.kpis.mobile { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.5rem; }
|
||||||
.kpi {
|
.kpi {
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.95rem 1rem 0.85rem;
|
padding: 0.95rem 1rem 0.85rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.22rem;
|
gap: 0.22rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.kpi strong { font-size: 1.55rem; font-weight: 800; color: var(--ink); line-height: 1.1; }
|
.kpi strong { font-size: 1.55rem; font-weight: 600; color: var(--ink); line-height: 1.1; }
|
||||||
.kpi span { color: var(--muted); font-size: 0.82rem; }
|
.kpi span { color: var(--muted); font-size: 0.82rem; }
|
||||||
.kpi.teal { background: #e7f7f4; }
|
.kpi.teal { background: #e7f7f4; }
|
||||||
.kpi.purple { background: #f3eefc; }
|
.kpi.purple { background: #f3eefc; }
|
||||||
.kpi.gold { background: #fff6df; }
|
.kpi.gold { background: #fff6df; }
|
||||||
.kpi.blue { background: #e8f3fb; }
|
.kpi.blue { background: #e8f3fb; }
|
||||||
.kpi-ico {
|
.kpi-ico {
|
||||||
width: 34px; height: 34px; border-radius: 10px;
|
width: 34px; height: 34px; border-radius: var(--radius);
|
||||||
display: grid; place-items: center; margin-bottom: 0.15rem;
|
display: grid; place-items: center; margin-bottom: 0.15rem;
|
||||||
}
|
}
|
||||||
.kpi.teal .kpi-ico { background: #c7efe8; color: #0f766e; }
|
.kpi.teal .kpi-ico { background: #c7efe8; color: #0f766e; }
|
||||||
@@ -1093,7 +1093,7 @@ h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
.tabs button {
|
.tabs button {
|
||||||
@@ -1102,7 +1102,7 @@ h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
|||||||
color: var(--teal-deep);
|
color: var(--teal-deep);
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
height: var(--control-h);
|
height: var(--control-h);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -1115,7 +1115,7 @@ h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
|||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem 1.05rem 1.1rem;
|
padding: 1rem 1.05rem 1.1rem;
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1141,26 +1141,33 @@ h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
|||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
height: var(--control-h);
|
height: var(--control-h);
|
||||||
padding: 0 0.95rem;
|
padding: 0 0.85rem;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
.btn.primary { background: var(--teal); color: #fff; box-shadow: 0 8px 16px rgba(20, 184, 166, 0.22); }
|
.btn.primary { background: var(--accent); color: #fff; }
|
||||||
.btn.ghost, .btn.settings {
|
.btn.ghost, .btn.settings {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--teal-deep);
|
color: var(--ink-secondary);
|
||||||
border-color: #99f6e4;
|
border-color: var(--line);
|
||||||
}
|
}
|
||||||
.btn.sm { min-height: 34px; padding: 0.3rem 0.7rem; font-size: 0.85rem; }
|
.btn.ghost:hover, .btn.settings:hover {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
}
|
||||||
|
.btn.sm { min-height: 30px; height: 30px; padding: 0 0.65rem; font-size: var(--fs-xs); }
|
||||||
.btn:disabled { opacity: 0.55; cursor: not-allowed; }
|
.btn:disabled { opacity: 0.55; cursor: not-allowed; }
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
width: 40px; height: 40px; border-radius: 12px;
|
width: var(--control-h); height: var(--control-h); border-radius: var(--radius);
|
||||||
border: 1px solid #e2e8f0; background: #fff; color: #64748b;
|
border: 1px solid var(--line); background: #fff; color: var(--muted);
|
||||||
display: grid; place-items: center;
|
display: grid; place-items: center; box-sizing: border-box;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
.icon-btn.on { background: #ccfbf1; border-color: #99f6e4; color: #0f766e; }
|
.icon-btn.on { background: var(--accent-subtle); border-color: var(--accent); color: var(--accent-deep); }
|
||||||
.text-link {
|
.text-link {
|
||||||
background: none; border: 0; color: var(--teal); font-weight: 700; padding: 0; justify-self: start;
|
background: none; border: 0; color: var(--teal); font-weight: 700; padding: 0; justify-self: start;
|
||||||
}
|
}
|
||||||
@@ -1173,7 +1180,7 @@ h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
|||||||
}
|
}
|
||||||
.level-card {
|
.level-card {
|
||||||
border: 1px solid #eef2f7;
|
border: 1px solid #eef2f7;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem 0.9rem;
|
padding: 1rem 0.9rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.28rem;
|
gap: 0.28rem;
|
||||||
@@ -1185,7 +1192,7 @@ h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
|||||||
.level-card.silver { background: linear-gradient(180deg, #f3f6fa, #fff); }
|
.level-card.silver { background: linear-gradient(180deg, #f3f6fa, #fff); }
|
||||||
.level-card.bronze { background: linear-gradient(180deg, #fff3e6, #fff); }
|
.level-card.bronze { background: linear-gradient(180deg, #fff3e6, #fff); }
|
||||||
.lvl-ico {
|
.lvl-ico {
|
||||||
width: 42px; height: 42px; border-radius: 12px;
|
width: 42px; height: 42px; border-radius: var(--radius);
|
||||||
display: grid; place-items: center; margin-bottom: 0.2rem;
|
display: grid; place-items: center; margin-bottom: 0.2rem;
|
||||||
}
|
}
|
||||||
.level-card.gold .lvl-ico { background: #ffe58a; color: #ca8a04; }
|
.level-card.gold .lvl-ico { background: #ffe58a; color: #ca8a04; }
|
||||||
@@ -1232,18 +1239,29 @@ h2 { margin: 0; font-size: 1.08rem; color: var(--ink); }
|
|||||||
}
|
}
|
||||||
.search {
|
.search {
|
||||||
display: flex; align-items: center; gap: 0.4rem;
|
display: flex; align-items: center; gap: 0.4rem;
|
||||||
border: 1px solid #e2e8f0; border-radius: 12px; padding: 0 0.75rem;
|
border: 1px solid var(--line); border-radius: var(--radius); padding: 0 0.75rem;
|
||||||
background: #fff; color: #94a3b8; min-width: 220px;
|
background: #fff; color: var(--muted); min-width: 200px; max-width: 360px;
|
||||||
|
height: var(--control-h); min-height: var(--control-h); box-sizing: border-box;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
}
|
}
|
||||||
.search input, select, textarea, .form input, .form select, .form textarea {
|
.search:focus-within {
|
||||||
border: 0; background: none; font: inherit; min-width: 0; width: 100%;
|
border-color: var(--accent);
|
||||||
padding: 0.65rem 0;
|
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||||
}
|
}
|
||||||
select, .form input, .form select, .form textarea {
|
.search input {
|
||||||
border: 1px solid #e2e8f0; border-radius: 12px; padding: 0.65rem 0.8rem; background: #fff;
|
border: 0; outline: none; background: none; font: inherit; font-size: var(--fs-sm);
|
||||||
|
min-width: 0; width: 100%; color: var(--ink); padding: 0;
|
||||||
|
}
|
||||||
|
select, textarea, .form input, .form select, .form textarea {
|
||||||
|
border: 1px solid var(--line); border-radius: var(--radius); padding: 0 0.75rem; background: #fff;
|
||||||
|
min-height: var(--control-h); height: var(--control-h); font: inherit; font-size: var(--fs-sm);
|
||||||
|
box-sizing: border-box; color: var(--ink);
|
||||||
|
}
|
||||||
|
textarea, .form textarea {
|
||||||
|
height: auto; min-height: 80px; padding: 0.5rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrap { overflow: auto; border: 1px solid #eef2f7; border-radius: 14px; }
|
.table-wrap { overflow: auto; border: 1px solid #eef2f7; border-radius: var(--radius); }
|
||||||
table { width: 100%; border-collapse: collapse; min-width: 860px; }
|
table { width: 100%; border-collapse: collapse; min-width: 860px; }
|
||||||
th, td { padding: 0.8rem 0.65rem; border-bottom: 1px solid #eef2f7; text-align: right; font-size: 0.9rem; }
|
th, td { padding: 0.8rem 0.65rem; border-bottom: 1px solid #eef2f7; text-align: right; font-size: 0.9rem; }
|
||||||
th { background: #f8fafc; color: #64748b; font-weight: 700; }
|
th { background: #f8fafc; color: #64748b; font-weight: 700; }
|
||||||
@@ -1251,7 +1269,7 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; }
|
|||||||
.who { display: flex; align-items: center; gap: 0.55rem; }
|
.who { display: flex; align-items: center; gap: 0.55rem; }
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 36px; height: 36px; border-radius: 999px; background: #ccfbf1; color: #0f766e;
|
width: 36px; height: 36px; border-radius: 999px; background: #ccfbf1; color: #0f766e;
|
||||||
display: grid; place-items: center; font-weight: 800; font-size: 0.75rem; flex-shrink: 0;
|
display: grid; place-items: center; font-weight: 600; font-size: 0.75rem; flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-flex; align-items: center; gap: 0.25rem;
|
display: inline-flex; align-items: center; gap: 0.25rem;
|
||||||
@@ -1261,7 +1279,7 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; }
|
|||||||
.badge.silver { background: #e2e8f0; color: #475569; }
|
.badge.silver { background: #e2e8f0; color: #475569; }
|
||||||
.badge.bronze { background: #ffedd5; color: #c2410c; }
|
.badge.bronze { background: #ffedd5; color: #c2410c; }
|
||||||
.badge.custom { background: #ccfbf1; color: #0f766e; }
|
.badge.custom { background: #ccfbf1; color: #0f766e; }
|
||||||
.pts { font-weight: 800; }
|
.pts { font-weight: 600; }
|
||||||
.ops { position: relative; display: inline-flex; gap: 0.3rem; align-items: center; }
|
.ops { position: relative; display: inline-flex; gap: 0.3rem; align-items: center; }
|
||||||
.act {
|
.act {
|
||||||
width: 30px; height: 30px; border: 0; border-radius: 999px; display: grid; place-items: center; cursor: pointer;
|
width: 30px; height: 30px; border: 0; border-radius: 999px; display: grid; place-items: center; cursor: pointer;
|
||||||
@@ -1271,7 +1289,7 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; }
|
|||||||
.act.more { background: #f1f5f9; color: #64748b; }
|
.act.more { background: #f1f5f9; color: #64748b; }
|
||||||
.menu {
|
.menu {
|
||||||
position: absolute; inset-inline-end: 0; top: 34px; z-index: 5;
|
position: absolute; inset-inline-end: 0; top: 34px; z-index: 5;
|
||||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
|
background: #fff; border: 1px solid #e2e8f0; border-radius: var(--radius);
|
||||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
|
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12);
|
||||||
display: grid; min-width: 140px; overflow: hidden;
|
display: grid; min-width: 140px; overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -1323,7 +1341,7 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; }
|
|||||||
}
|
}
|
||||||
.mob-pts em {
|
.mob-pts em {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
@@ -1332,7 +1350,7 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; }
|
|||||||
|
|
||||||
.reward-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.7rem; }
|
.reward-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.7rem; }
|
||||||
.reward-card {
|
.reward-card {
|
||||||
border: 1px solid #eef2f7; border-radius: 16px; padding: 0.95rem; display: grid; gap: 0.3rem;
|
border: 1px solid #eef2f7; border-radius: var(--radius); padding: 0.95rem; display: grid; gap: 0.3rem;
|
||||||
}
|
}
|
||||||
.reward-card.off { opacity: 0.55; }
|
.reward-card.off { opacity: 0.55; }
|
||||||
.reward-card p, .reward-card small { margin: 0; color: var(--muted); font-size: 0.84rem; }
|
.reward-card p, .reward-card small { margin: 0; color: var(--muted); font-size: 0.84rem; }
|
||||||
@@ -1341,15 +1359,15 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; }
|
|||||||
.overlay {
|
.overlay {
|
||||||
position: fixed; inset: 0; z-index: 80;
|
position: fixed; inset: 0; z-index: 80;
|
||||||
background: rgba(15, 23, 42, 0.35);
|
background: rgba(15, 23, 42, 0.35);
|
||||||
display: grid; place-items: center; padding: 1rem;
|
display: grid; place-items: center; padding: 16px;
|
||||||
}
|
}
|
||||||
.sheet {
|
.sheet {
|
||||||
width: min(460px, 100%);
|
width: min(460px, 100%);
|
||||||
background: #fff; border-radius: 18px; padding: 1rem 1.05rem 1.1rem;
|
background: #fff; border-radius: var(--radius, 10px); padding: 16px;
|
||||||
display: grid; gap: 0.8rem;
|
display: grid; gap: 16px;
|
||||||
}
|
}
|
||||||
.sheet header { display: flex; justify-content: space-between; align-items: center; }
|
.sheet header { display: flex; justify-content: space-between; align-items: center; }
|
||||||
.sheet h3 { margin: 0; font-size: 1.05rem; }
|
.sheet h3 { margin: 0; font-size: 16px; font-weight: 500; color: var(--ink); line-height: 1.4; }
|
||||||
.form, .ledger { display: grid; gap: 0.7rem; }
|
.form, .ledger { display: grid; gap: 0.7rem; }
|
||||||
.form label { display: grid; gap: 0.3rem; font-size: 0.88rem; font-weight: 700; color: #334155; }
|
.form label { display: grid; gap: 0.3rem; font-size: 0.88rem; font-weight: 700; color: #334155; }
|
||||||
.check-row { display: flex; align-items: center; gap: 0.45rem; font-weight: 700; }
|
.check-row { display: flex; align-items: center; gap: 0.45rem; font-weight: 700; }
|
||||||
@@ -1359,7 +1377,7 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; }
|
|||||||
.form footer { display: flex; justify-content: flex-end; gap: 0.45rem; }
|
.form footer { display: flex; justify-content: flex-end; gap: 0.45rem; }
|
||||||
.signs { display: flex; gap: 0.4rem; }
|
.signs { display: flex; gap: 0.4rem; }
|
||||||
.signs button {
|
.signs button {
|
||||||
flex: 1; border: 1px solid #e2e8f0; background: #fff; border-radius: 10px;
|
flex: 1; border: 1px solid #e2e8f0; background: #fff; border-radius: var(--radius);
|
||||||
min-height: 38px; font: inherit; font-weight: 700; cursor: pointer;
|
min-height: 38px; font: inherit; font-weight: 700; cursor: pointer;
|
||||||
}
|
}
|
||||||
.signs button.on { background: #ccfbf1; border-color: #99f6e4; color: var(--teal-deep); }
|
.signs button.on { background: #ccfbf1; border-color: #99f6e4; color: var(--teal-deep); }
|
||||||
|
|||||||
@@ -1214,17 +1214,18 @@ onBeforeUnmount(() => {
|
|||||||
--note-default: #ffffff;
|
--note-default: #ffffff;
|
||||||
--teal: #0d9488;
|
--teal: #0d9488;
|
||||||
--teal-soft: #ccfbf1;
|
--teal-soft: #ccfbf1;
|
||||||
--radius: 15px;
|
--radius: 10px;
|
||||||
--shadow: 0 4px 18px rgba(15, 23, 42, 0.07);
|
--shadow: 0 4px 18px rgba(15, 23, 42, 0.07);
|
||||||
max-width: 1200px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
max-width: 100%;
|
||||||
padding: 0.5rem 0 4rem;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-head h1 {
|
.page-head h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.55rem;
|
font-size: 1.55rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
}
|
}
|
||||||
.lead {
|
.lead {
|
||||||
@@ -1236,7 +1237,7 @@ onBeforeUnmount(() => {
|
|||||||
.banner-error {
|
.banner-error {
|
||||||
margin: 0.75rem 0 0;
|
margin: 0.75rem 0 0;
|
||||||
padding: 0.65rem 0.85rem;
|
padding: 0.65rem 0.85rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #fef2f2;
|
background: #fef2f2;
|
||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -1339,7 +1340,7 @@ onBeforeUnmount(() => {
|
|||||||
.view-toggle {
|
.view-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@@ -1356,7 +1357,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
.sort-select {
|
.sort-select {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.35rem 0.55rem;
|
padding: 0.35rem 0.55rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
@@ -1367,7 +1368,7 @@ onBeforeUnmount(() => {
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -1420,7 +1421,7 @@ onBeforeUnmount(() => {
|
|||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: var(--teal);
|
background: var(--teal);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -1518,7 +1519,7 @@ onBeforeUnmount(() => {
|
|||||||
z-index: 5;
|
z-index: 5;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
@@ -1530,7 +1531,7 @@ onBeforeUnmount(() => {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
padding: 0.45rem 0.65rem;
|
padding: 0.45rem 0.65rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
@@ -1591,7 +1592,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.card-thumb {
|
.card-thumb {
|
||||||
margin-top: 0.45rem;
|
margin-top: 0.45rem;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
max-height: 72px;
|
max-height: 72px;
|
||||||
@@ -1609,7 +1610,7 @@ onBeforeUnmount(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
background: rgba(15, 23, 42, 0.06);
|
background: rgba(15, 23, 42, 0.06);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
@@ -1750,7 +1751,7 @@ onBeforeUnmount(() => {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: min(520px, 100%);
|
width: min(520px, 100%);
|
||||||
max-height: min(92vh, 840px);
|
max-height: min(92vh, 840px);
|
||||||
border-radius: 18px;
|
border-radius: var(--radius, 10px);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.28);
|
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.28);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1774,25 +1775,28 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.85rem 1rem;
|
padding: 16px;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
}
|
}
|
||||||
.panel-head h2 {
|
.panel-head h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.05rem;
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--ink);
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
.panel-scroll {
|
.panel-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 1rem;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
.panel-foot {
|
.panel-foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 12px;
|
||||||
padding: 0.85rem 1rem;
|
padding: 16px;
|
||||||
border-top: 1px solid #e2e8f0;
|
border-top: 1px solid #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1808,7 +1812,7 @@ onBeforeUnmount(() => {
|
|||||||
.body-canvas {
|
.body-canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
min-height: 160px;
|
min-height: 160px;
|
||||||
@@ -1897,7 +1901,7 @@ onBeforeUnmount(() => {
|
|||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.35rem 0.55rem;
|
padding: 0.35rem 0.55rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
@@ -1920,7 +1924,7 @@ onBeforeUnmount(() => {
|
|||||||
.field select,
|
.field select,
|
||||||
.field input[type='text'] {
|
.field input[type='text'] {
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
padding: 0.45rem 0.55rem;
|
padding: 0.45rem 0.55rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
@@ -1937,7 +1941,7 @@ onBeforeUnmount(() => {
|
|||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 0.7rem 0.75rem;
|
padding: 0.7rem 0.75rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #f0fdfa;
|
background: #f0fdfa;
|
||||||
border: 1px solid #99f6e4;
|
border: 1px solid #99f6e4;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1966,7 +1970,7 @@ onBeforeUnmount(() => {
|
|||||||
.check-row input[type='text'] {
|
.check-row input[type='text'] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
padding: 0.35rem 0.5rem;
|
padding: 0.35rem 0.5rem;
|
||||||
}
|
}
|
||||||
.linkish {
|
.linkish {
|
||||||
@@ -1990,7 +1994,7 @@ onBeforeUnmount(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
padding: 0.25rem 0.45rem;
|
padding: 0.25rem 0.45rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
@@ -2007,7 +2011,7 @@ onBeforeUnmount(() => {
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: var(--teal);
|
background: var(--teal);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius, 10px);
|
||||||
padding: 0.55rem 1rem;
|
padding: 0.55rem 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -2021,7 +2025,7 @@ onBeforeUnmount(() => {
|
|||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius, 10px);
|
||||||
padding: 0.55rem 1rem;
|
padding: 0.55rem 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -2101,7 +2105,7 @@ onBeforeUnmount(() => {
|
|||||||
.sheet {
|
.sheet {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px 18px 0 0;
|
border-radius: var(--radius) var(--radius) 0 0;
|
||||||
padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom));
|
padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom));
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -2174,7 +2178,7 @@ onBeforeUnmount(() => {
|
|||||||
.editor-panel {
|
.editor-panel {
|
||||||
width: min(100%, 520px);
|
width: min(100%, 520px);
|
||||||
max-height: 94vh;
|
max-height: 94vh;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
.panel-root {
|
.panel-root {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ import CustomerMoodFace from '@/components/CustomerMoodFace.vue';
|
|||||||
import EntityTimeline from '@/components/EntityTimeline.vue';
|
import EntityTimeline from '@/components/EntityTimeline.vue';
|
||||||
import MentoMemoryPanel from '@/components/MentoMemoryPanel.vue';
|
import MentoMemoryPanel from '@/components/MentoMemoryPanel.vue';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
import {
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiField,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
UiSelect,
|
||||||
|
UiTextarea,
|
||||||
|
} from '@/components/ui';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -157,371 +166,210 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" :class="{ embedded }">
|
<section class="crm-page" :class="{ embedded, narrow: !embedded }">
|
||||||
<div v-if="embedded" class="embed-actions">
|
<!-- Desktop Header -->
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="submit">
|
<UiPageHeader
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
v-if="!embedded"
|
||||||
</button>
|
:title="pageTitle"
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
|
:back-to="backTo"
|
||||||
</div>
|
>
|
||||||
<header v-if="!embedded" class="desktop-head">
|
<template #lead>
|
||||||
<div class="title-block">
|
<span class="crumb">
|
||||||
<span class="doc-ico"><UiIcon name="user" :size="22" /></span>
|
<RouterLink :to="{ name: 'contacts' }">مخاطبین</RouterLink>
|
||||||
<div>
|
<span>›</span>
|
||||||
<h1>{{ pageTitle }}</h1>
|
<strong>{{ pageTitle }}</strong>
|
||||||
<p class="crumb">
|
</span>
|
||||||
<RouterLink :to="{ name: 'contacts' }">مخاطبین</RouterLink>
|
</template>
|
||||||
<span>›</span>
|
<template #actions>
|
||||||
<strong>{{ pageTitle }}</strong>
|
<UiButton variant="primary" :disabled="saving" @click="submit">
|
||||||
</p>
|
<UiIcon name="check" :size="16" />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="desk-actions">
|
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="submit">
|
|
||||||
<UiIcon name="check-plain" :size="16" />
|
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
|
<UiButton variant="ghost" :disabled="saving" @click="cancel">انصراف</UiButton>
|
||||||
<button v-if="!isNew" class="btn danger" type="button" :disabled="saving" @click="remove">
|
<UiButton v-if="!isNew" variant="danger" :disabled="saving" @click="remove">
|
||||||
حذف
|
حذف
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</template>
|
||||||
</header>
|
</UiPageHeader>
|
||||||
|
|
||||||
<header v-if="!embedded" class="mobile-head">
|
|
||||||
<button class="icon-btn" type="button" aria-label="ذخیره" :disabled="saving" @click="submit">
|
|
||||||
<UiIcon name="check-plain" :size="22" />
|
|
||||||
</button>
|
|
||||||
<h1>{{ pageTitle }}</h1>
|
|
||||||
<button class="icon-btn" type="button" aria-label="بستن" @click="cancel">
|
|
||||||
<UiIcon name="x" :size="20" />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
||||||
<form v-else class="form" @submit.prevent="submit">
|
<form v-else class="crm-form" @submit.prevent="submit">
|
||||||
<section class="card">
|
<UiCard>
|
||||||
<div class="grid">
|
<div class="crm-form-grid-2">
|
||||||
<div class="field">
|
<UiField label="نام" required for-id="firstName">
|
||||||
<label for="firstName">نام</label>
|
<UiInput id="firstName" v-model="firstName" required />
|
||||||
<input id="firstName" v-model="firstName" required />
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="نام خانوادگی" required for-id="lastName">
|
||||||
|
<UiInput id="lastName" v-model="lastName" required />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="موبایل" for-id="mobile">
|
||||||
|
<UiInput id="mobile" v-model="mobile" type="tel" />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="تلفن" for-id="phone">
|
||||||
|
<UiInput id="phone" v-model="phone" type="tel" />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="ایمیل" for-id="email">
|
||||||
|
<UiInput id="email" v-model="email" type="email" />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="سمت" for-id="position">
|
||||||
|
<UiInput id="position" v-model="position" />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<div class="crm-col-span-full">
|
||||||
|
<UiField label="وضعیت مشتری">
|
||||||
|
<div class="moods">
|
||||||
|
<button
|
||||||
|
v-for="item in PERSON_MOODS"
|
||||||
|
:key="item"
|
||||||
|
class="mood-btn"
|
||||||
|
type="button"
|
||||||
|
:class="{ on: mood === item }"
|
||||||
|
@click="mood = item"
|
||||||
|
>
|
||||||
|
<CustomerMoodFace :mood="item" :size="40" />
|
||||||
|
<span>{{ PERSON_MOOD_LABELS[item] }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</UiField>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label for="lastName">نام خانوادگی</label>
|
<div class="crm-col-span-full">
|
||||||
<input id="lastName" v-model="lastName" required />
|
<UiField label="شرکت" for-id="companyId">
|
||||||
|
<UiSelect id="companyId" v-model="companyId">
|
||||||
|
<option value="">بدون شرکت</option>
|
||||||
|
<option v-for="c in companies" :key="c.id" :value="c.id">{{ c.name }}</option>
|
||||||
|
</UiSelect>
|
||||||
|
</UiField>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label for="mobile">موبایل</label>
|
<div class="crm-col-span-full">
|
||||||
<input id="mobile" v-model="mobile" inputmode="tel" />
|
<UiField label="یادداشت" for-id="notes">
|
||||||
|
<UiTextarea id="notes" v-model="notes" :rows="3" />
|
||||||
|
</UiField>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label for="phone">تلفن</label>
|
<div v-if="!isNew" class="crm-col-span-full loyalty-card">
|
||||||
<input id="phone" v-model="phone" inputmode="tel" />
|
<div class="loyalty-info">
|
||||||
</div>
|
<span class="muted">باشگاه مشتریان</span>
|
||||||
<div class="field">
|
<strong>{{ loyaltyPoints }} امتیاز<span v-if="loyaltyTierName"> · سطح {{ loyaltyTierName }}</span></strong>
|
||||||
<label for="email">ایمیل</label>
|
|
||||||
<input id="email" v-model="email" type="email" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="position">سمت</label>
|
|
||||||
<input id="position" v-model="position" />
|
|
||||||
</div>
|
|
||||||
<div class="field wide">
|
|
||||||
<label>وضعیت مشتری</label>
|
|
||||||
<div class="moods">
|
|
||||||
<button
|
|
||||||
v-for="item in PERSON_MOODS"
|
|
||||||
:key="item"
|
|
||||||
class="mood"
|
|
||||||
type="button"
|
|
||||||
:class="{ on: mood === item }"
|
|
||||||
@click="mood = item"
|
|
||||||
>
|
|
||||||
<CustomerMoodFace :mood="item" :size="44" />
|
|
||||||
<span>{{ PERSON_MOOD_LABELS[item] }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<RouterLink class="loyalty-link" :to="{ name: 'loyalty' }">مدیریت باشگاه</RouterLink>
|
||||||
<div class="field wide">
|
|
||||||
<label for="companyId">شرکت</label>
|
|
||||||
<select id="companyId" v-model="companyId">
|
|
||||||
<option value="">بدون شرکت</option>
|
|
||||||
<option v-for="c in companies" :key="c.id" :value="c.id">{{ c.name }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field wide">
|
|
||||||
<label for="notes">یادداشت</label>
|
|
||||||
<textarea id="notes" v-model="notes" rows="4" />
|
|
||||||
</div>
|
|
||||||
<div v-if="!isNew" class="field wide loyalty-box">
|
|
||||||
<p class="muted">باشگاه مشتریان</p>
|
|
||||||
<p>
|
|
||||||
{{ loyaltyPoints }} امتیاز
|
|
||||||
<span v-if="loyaltyTierName"> · سطح {{ loyaltyTierName }}</span>
|
|
||||||
</p>
|
|
||||||
<RouterLink :to="{ name: 'loyalty' }">مدیریت باشگاه</RouterLink>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</UiCard>
|
||||||
|
|
||||||
<CustomFieldsSection entity-type="person" v-model="customFields" />
|
<CustomFieldsSection entity-type="person" v-model="customFields" />
|
||||||
<MentoMemoryPanel v-if="!isNew && id" :person-id="id" />
|
<MentoMemoryPanel v-if="!isNew && id" :person-id="id" />
|
||||||
<EntityTimeline v-if="!isNew && id" entity-type="person" :entity-id="id" />
|
<EntityTimeline v-if="!isNew && id" entity-type="person" :entity-id="id" />
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="crm-field-error">{{ error }}</p>
|
||||||
|
|
||||||
<div class="mobile-actions">
|
<div class="mobile-actions mob-only">
|
||||||
<button class="btn primary" type="submit" :disabled="saving">
|
<UiButton variant="primary" block type="submit" :disabled="saving">
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
||||||
</button>
|
</UiButton>
|
||||||
<button v-if="!isNew" class="btn danger" type="button" :disabled="saving" @click="remove">
|
<UiButton v-if="!isNew" variant="danger" block type="button" :disabled="saving" @click="remove">
|
||||||
حذف
|
حذف
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Embedded Modal Actions (Fixed at bottom) -->
|
||||||
|
<div v-if="embedded" class="embed-actions">
|
||||||
|
<UiButton variant="primary" :disabled="saving" @click="submit">
|
||||||
|
<UiIcon name="check" :size="16" />
|
||||||
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="ghost" :disabled="saving" @click="cancel">انصراف</UiButton>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.crumb {
|
||||||
display: grid;
|
display: inline-flex;
|
||||||
gap: 0.85rem;
|
align-items: center;
|
||||||
max-width: 820px;
|
gap: var(--sp-1);
|
||||||
min-width: 0;
|
color: var(--muted);
|
||||||
padding-bottom: 1.5rem;
|
font-size: var(--fs-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.embedded {
|
.crumb a {
|
||||||
max-width: none;
|
color: var(--accent-deep);
|
||||||
padding-bottom: 0;
|
font-weight: var(--fw-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.embed-actions {
|
.embed-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
gap: var(--sp-2);
|
||||||
gap: 0.5rem;
|
margin-bottom: var(--sp-3);
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-head {
|
|
||||||
display: none;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-block {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-ico {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 14px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: #ecfdf5;
|
|
||||||
color: #0f766e;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crumb {
|
|
||||||
margin: 0.2rem 0 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crumb a {
|
|
||||||
color: #0f766e;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desk-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-head {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 40px 1fr 40px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-head h1 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-btn {
|
|
||||||
width: var(--control-h);
|
|
||||||
height: var(--control-h);
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
color: #0f766e;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 0.95rem;
|
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field.wide {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field label {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field input,
|
|
||||||
.field select,
|
|
||||||
.field textarea {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.85rem;
|
|
||||||
font: inherit;
|
|
||||||
background: #fff;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field textarea {
|
|
||||||
min-height: 5.5rem;
|
|
||||||
padding: 0.65rem 0.85rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.moods {
|
.moods {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mood {
|
.mood-btn {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 0.25rem;
|
gap: 4px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--line);
|
||||||
background: #fff;
|
background: var(--surface);
|
||||||
border-radius: 14px;
|
|
||||||
padding: 0.45rem 0.55rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font: inherit;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mood.on {
|
|
||||||
border-color: #14b8a6;
|
|
||||||
background: #f0fdfa;
|
|
||||||
color: #0f766e;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loyalty-box {
|
|
||||||
background: #f8fafc;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0.75rem 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loyalty-box a {
|
|
||||||
color: #0f766e;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted {
|
|
||||||
color: #64748b;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #b91c1c;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.95rem;
|
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
font-weight: 700;
|
padding: var(--sp-2) var(--sp-3);
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.mood-btn.on {
|
||||||
background: #14b8a6;
|
border-color: var(--accent);
|
||||||
color: #fff;
|
background: var(--accent-subtle);
|
||||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
color: var(--accent-deep);
|
||||||
|
font-weight: var(--fw-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.cancel {
|
.loyalty-card {
|
||||||
background: #fff;
|
display: flex;
|
||||||
color: #64748b;
|
align-items: center;
|
||||||
border-color: #e2e8f0;
|
justify-content: space-between;
|
||||||
|
padding: var(--sp-3) var(--sp-4);
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.danger {
|
.loyalty-info {
|
||||||
background: #fff;
|
display: flex;
|
||||||
color: #b91c1c;
|
flex-direction: column;
|
||||||
border-color: #fecaca;
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loyalty-link {
|
||||||
|
color: var(--accent-deep);
|
||||||
|
font-weight: var(--fw-semibold);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-actions {
|
.mobile-actions {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 0.5rem;
|
flex-direction: column;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
margin-top: var(--sp-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 960px) {
|
@media (min-width: 901px) {
|
||||||
.desktop-head {
|
.mob-only {
|
||||||
display: flex;
|
display: none !important;
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-head,
|
|
||||||
.mobile-actions {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -928,9 +928,8 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
.page {
|
.page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 1180px;
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0;
|
||||||
padding-bottom: 6.4rem;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
@@ -971,7 +970,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.55rem 0.9rem;
|
padding: 0.55rem 0.9rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -1030,7 +1029,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 22px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -1054,7 +1053,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0.22rem 0.7rem;
|
padding: 0.22rem 0.7rem;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
background: #ecfdf5;
|
background: #ecfdf5;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
}
|
}
|
||||||
@@ -1103,7 +1102,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
background: color-mix(in srgb, #0f766e 12%, transparent);
|
background: color-mix(in srgb, #0f766e 12%, transparent);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
padding: 0.12rem 0.5rem;
|
padding: 0.12rem 0.5rem;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
@@ -1215,7 +1214,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
background: #ecfdf5;
|
background: #ecfdf5;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1284,7 +1283,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem 1.05rem 0.95rem;
|
padding: 1rem 1.05rem 0.95rem;
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1339,7 +1338,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.14);
|
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.14);
|
||||||
min-width: 8.5rem;
|
min-width: 8.5rem;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
@@ -1351,7 +1350,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
background: none;
|
background: none;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
padding: 0.5rem 0.65rem;
|
padding: 0.5rem 0.65rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -1434,7 +1433,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
color: #b45309;
|
color: #b45309;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
padding: 0.18rem 0.55rem;
|
padding: 0.18rem 0.55rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1478,10 +1477,10 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
border: 1.5px solid #14b8a6;
|
border: 1.5px solid #14b8a6;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.62rem 0.8rem;
|
padding: 0.62rem 0.8rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -1506,7 +1505,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.55rem 0.65rem;
|
padding: 0.55rem 0.65rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1530,7 +1529,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
.file-ico {
|
.file-ico {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: #fee2e2;
|
background: #fee2e2;
|
||||||
@@ -1546,7 +1545,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.15rem;
|
gap: 0.15rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
padding: 0.85rem 0.95rem;
|
padding: 0.85rem 0.95rem;
|
||||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
@@ -1567,7 +1566,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
.info-panel dl {
|
.info-panel dl {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.4rem 1rem;
|
padding: 0.4rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1597,7 +1596,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
.smart {
|
.smart {
|
||||||
background: linear-gradient(135deg, #ecfeff, #f5f3ff);
|
background: linear-gradient(135deg, #ecfeff, #f5f3ff);
|
||||||
border: 1px solid #e0e7ff;
|
border: 1px solid #e0e7ff;
|
||||||
border-radius: 20px;
|
border-radius: var(--radius);
|
||||||
padding: 0.95rem 1rem 0.85rem;
|
padding: 0.95rem 1rem 0.85rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -1628,7 +1627,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
.smart-cta {
|
.smart-cta {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #ccfbf1;
|
background: #ccfbf1;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -1699,7 +1698,7 @@ watch(() => route.query.tab, readTab, { immediate: true });
|
|||||||
|
|
||||||
.fab-label {
|
.fab-label {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ h3 {
|
|||||||
.card {
|
.card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #d7e0db;
|
border: 1px solid #d7e0db;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
@@ -475,7 +475,7 @@ label {
|
|||||||
input,
|
input,
|
||||||
textarea {
|
textarea {
|
||||||
border: 1px solid #cfd9d3;
|
border: 1px solid #cfd9d3;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.7rem 0.85rem;
|
padding: 0.7rem 0.85rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -484,7 +484,7 @@ textarea {
|
|||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.7rem 1rem;
|
padding: 0.7rem 1rem;
|
||||||
background: #0f6b54;
|
background: #0f6b54;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -531,7 +531,7 @@ textarea {
|
|||||||
|
|
||||||
.msg {
|
.msg {
|
||||||
border: 1px solid #e0e8e3;
|
border: 1px solid #e0e8e3;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0.7rem 0.85rem;
|
padding: 0.7rem 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,7 +558,7 @@ textarea {
|
|||||||
.score {
|
.score {
|
||||||
width: 2.3rem;
|
width: 2.3rem;
|
||||||
height: 2.3rem;
|
height: 2.3rem;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
border: 1px solid #cfd9d3;
|
border: 1px solid #cfd9d3;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import MoneyInput from '@/components/MoneyInput.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
|
import {
|
||||||
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiField,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
UiSegmented,
|
||||||
|
UiSelect,
|
||||||
|
UiTextarea,
|
||||||
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import type {
|
import type {
|
||||||
CreateProductRequest,
|
CreateProductRequest,
|
||||||
@@ -45,6 +56,11 @@ const error = ref('');
|
|||||||
const enabledCurrencies = ref<string[]>(['IRR']);
|
const enabledCurrencies = ref<string[]>(['IRR']);
|
||||||
|
|
||||||
const kind = ref<'product' | 'service'>('product');
|
const kind = ref<'product' | 'service'>('product');
|
||||||
|
const kindOptions = [
|
||||||
|
{ value: 'product' as const, label: 'کالا / محصول' },
|
||||||
|
{ value: 'service' as const, label: 'خدمت' },
|
||||||
|
];
|
||||||
|
|
||||||
const name = ref('');
|
const name = ref('');
|
||||||
const category = ref('');
|
const category = ref('');
|
||||||
const sku = ref('');
|
const sku = ref('');
|
||||||
@@ -60,7 +76,7 @@ const country = ref('');
|
|||||||
const tagInput = ref('');
|
const tagInput = ref('');
|
||||||
const tags = ref<string[]>([]);
|
const tags = ref<string[]>([]);
|
||||||
|
|
||||||
const title = computed(() => (isEdit.value ? 'ویرایش محصول' : 'افزودن محصول'));
|
const title = computed(() => (isEdit.value ? 'ویرایش محصول' : 'افزودن محصول جدید'));
|
||||||
const submitLabel = computed(() =>
|
const submitLabel = computed(() =>
|
||||||
saving.value
|
saving.value
|
||||||
? 'در حال ثبت…'
|
? 'در حال ثبت…'
|
||||||
@@ -196,515 +212,335 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" :class="{ embedded }">
|
<section class="crm-page product-form-page" :class="{ embedded }" dir="rtl">
|
||||||
<div v-if="embedded" class="embed-actions">
|
<UiPageHeader
|
||||||
<button class="btn primary" type="button" :disabled="saving" @click="submit">
|
v-if="!embedded"
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
:title="title"
|
||||||
</button>
|
description="تعریف و ویرایش کالا و خدمات، قیمتگذاری و مدیریت موجودی انبار"
|
||||||
<button class="btn cancel" type="button" :disabled="saving" @click="cancel">انصراف</button>
|
>
|
||||||
</div>
|
<template #breadcrumb>
|
||||||
<header v-if="!embedded" class="page-head">
|
<RouterLink :to="{ name: 'products' }" class="breadcrumb-link">
|
||||||
<button type="button" class="back mob-only" aria-label="بازگشت" @click="cancel">
|
محصولات
|
||||||
<UiIcon name="chevron" :size="20" />
|
</RouterLink>
|
||||||
</button>
|
<span class="breadcrumb-separator">/</span>
|
||||||
<div class="title-row">
|
<span>{{ title }}</span>
|
||||||
<span class="title-ico" aria-hidden="true">
|
</template>
|
||||||
<UiIcon name="box" :size="22" />
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<h1>{{ title }}</h1>
|
|
||||||
<p class="lead">اطلاعات محصول را وارد کنید</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
<template #actions>
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<UiButton :loading="saving" @click="submit">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="check-plain" />
|
||||||
|
</template>
|
||||||
|
{{ submitLabel }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="ghost" :disabled="saving" @click="cancel">
|
||||||
|
انصراف
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiPageHeader>
|
||||||
|
|
||||||
<form v-else class="form" @submit.prevent="submit">
|
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||||||
<div class="kind-seg mob-only">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="{ on: kind === 'product' }"
|
|
||||||
@click="kind = 'product'"
|
|
||||||
>
|
|
||||||
<UiIcon name="box" :size="16" />
|
|
||||||
محصول
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="{ on: kind === 'service' }"
|
|
||||||
@click="kind = 'service'"
|
|
||||||
>
|
|
||||||
<UiIcon name="briefcase" :size="16" />
|
|
||||||
خدمت
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid">
|
<form class="crm-form product-form" @submit.prevent="submit">
|
||||||
<section class="card main">
|
<div class="form-layout-grid">
|
||||||
<h2>اطلاعات اصلی</h2>
|
<div class="main-column">
|
||||||
|
<UiCard title="مشخصات اصلی کالا / خدمت" class="section-card">
|
||||||
<label class="field kind-desk">
|
<div class="kind-selection-row">
|
||||||
<span>نوع</span>
|
<span class="kind-label">ماهیت آیتم:</span>
|
||||||
<div class="kind-seg">
|
<UiSegmented v-model="kind" :options="kindOptions" />
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="{ on: kind === 'product' }"
|
|
||||||
@click="kind = 'product'"
|
|
||||||
>
|
|
||||||
<UiIcon name="box" :size="16" />
|
|
||||||
محصول
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
:class="{ on: kind === 'service' }"
|
|
||||||
@click="kind = 'service'"
|
|
||||||
>
|
|
||||||
<UiIcon name="briefcase" :size="16" />
|
|
||||||
خدمت
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<div class="crm-form-grid-2">
|
||||||
<span>نام محصول <em>*</em></span>
|
<UiField label="نام کالا یا خدمت" required class="col-span-2">
|
||||||
<div class="control">
|
<UiInput
|
||||||
<UiIcon name="box" :size="18" />
|
v-model="name"
|
||||||
<input v-model="name" required placeholder="مثلاً انگشتر طلا مدل ونوس" />
|
required
|
||||||
</div>
|
placeholder="مثلاً لپتاپ لنوو مدل ThinkPad یا پشتیبانی سالانه نرمافزار"
|
||||||
</label>
|
/>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
<label class="field">
|
<UiField label="دستهبندی" required>
|
||||||
<span>دستهبندی <em>*</em></span>
|
<UiSelect v-model="category" required>
|
||||||
<div class="control">
|
<option disabled value="">انتخاب دستهبندی</option>
|
||||||
<UiIcon name="layers" :size="18" />
|
<option v-for="c in CATEGORIES" :key="c" :value="c">{{ c }}</option>
|
||||||
<select v-model="category" required>
|
</UiSelect>
|
||||||
<option disabled value="">دستهبندی را انتخاب کنید</option>
|
</UiField>
|
||||||
<option v-for="c in CATEGORIES" :key="c" :value="c">{{ c }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field">
|
<UiField label="کد کالا / شناسه انبار (SKU)" required>
|
||||||
<span>کد / SKU <em>*</em></span>
|
<UiInput
|
||||||
<div class="control">
|
v-model="sku"
|
||||||
<UiIcon name="flag" :size="18" />
|
required
|
||||||
<input v-model="sku" required placeholder="MNT-001" dir="ltr" />
|
placeholder="MNT-001"
|
||||||
</div>
|
dir="ltr"
|
||||||
</label>
|
/>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
<div class="row-2">
|
<UiField label="قیمت پایه / واحد" required>
|
||||||
<label class="field">
|
<div class="price-input-wrapper">
|
||||||
<span>قیمت واحد <em>*</em></span>
|
<MoneyInput v-model="unitPrice" :decimals="0" placeholder="مبلغ واحد" />
|
||||||
<div class="control price">
|
<span class="currency-unit-label">
|
||||||
<UiIcon name="dollar" :size="18" />
|
{{ currency === 'IRR' || currency === 'IRT' ? 'تومان' : currency }}
|
||||||
<MoneyInput v-model="unitPrice" :decimals="0" placeholder="مبلغ را وارد کنید" />
|
</span>
|
||||||
<span class="suffix">{{ currency === 'IRR' || currency === 'IRT' ? 'تومان' : currency }}</span>
|
</div>
|
||||||
</div>
|
</UiField>
|
||||||
</label>
|
|
||||||
<label class="field">
|
<UiField label="ارز قیمتگذاری">
|
||||||
<span>ارز</span>
|
<UiSelect v-model="currency">
|
||||||
<div class="control">
|
|
||||||
<select v-model="currency">
|
|
||||||
<option v-for="code in enabledCurrencies" :key="code" :value="code">
|
<option v-for="code in enabledCurrencies" :key="code" :value="code">
|
||||||
{{ code }}
|
{{ code }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
</div>
|
</UiField>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row-2">
|
<UiField label="واحد سنجش" required>
|
||||||
<label class="field">
|
<UiSelect v-model="unit" required>
|
||||||
<span>واحد سنجش <em>*</em></span>
|
|
||||||
<div class="control">
|
|
||||||
<UiIcon name="layers" :size="18" />
|
|
||||||
<select v-model="unit" required>
|
|
||||||
<option v-for="u in UNITS" :key="u" :value="u">{{ u }}</option>
|
<option v-for="u in UNITS" :key="u" :value="u">{{ u }}</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
</div>
|
</UiField>
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>موجودی</span>
|
|
||||||
<div class="control">
|
|
||||||
<UiIcon name="bag" :size="18" />
|
|
||||||
<input v-model="stockQty" type="number" min="0" placeholder="مثلاً ۱۲۰" dir="ltr" />
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status-row">
|
<UiField v-if="kind === 'product'" label="موجودی اولیه در انبار">
|
||||||
<span>وضعیت محصول</span>
|
<UiInput
|
||||||
<label class="switch">
|
v-model="stockQty"
|
||||||
<input v-model="isActive" type="checkbox" />
|
type="number"
|
||||||
<span class="track" />
|
min="0"
|
||||||
<span class="switch-text">این محصول فعال باشد</span>
|
placeholder="مثلاً ۱۲۰"
|
||||||
</label>
|
dir="ltr"
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="side">
|
|
||||||
<section class="card">
|
|
||||||
<h2>توضیحات</h2>
|
|
||||||
<label class="field">
|
|
||||||
<div class="control area">
|
|
||||||
<UiIcon name="document" :size="18" />
|
|
||||||
<textarea
|
|
||||||
v-model="description"
|
|
||||||
rows="5"
|
|
||||||
placeholder="توضیحات محصول را وارد کنید..."
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</UiField>
|
||||||
</label>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card desk-only">
|
<div class="status-toggle-box">
|
||||||
<h2>اطلاعات تکمیلی</h2>
|
<label class="toggle-switch-label">
|
||||||
<label class="field">
|
<input v-model="isActive" type="checkbox" />
|
||||||
<span>برند</span>
|
<span class="toggle-text">این آیتم فعال و در فاکتورها قابل انتخاب باشد</span>
|
||||||
<div class="control">
|
</label>
|
||||||
<UiIcon name="building" :size="18" />
|
</div>
|
||||||
<input v-model="brand" placeholder="برند را وارد کنید" />
|
</UiCard>
|
||||||
</div>
|
|
||||||
</label>
|
<UiCard title="شرح و توضیحات محصول" class="section-card">
|
||||||
<label class="field">
|
<UiField label="توضیحات کامل کالا یا شرایط خدمت">
|
||||||
<span>مدل</span>
|
<UiTextarea
|
||||||
<div class="control">
|
v-model="description"
|
||||||
<UiIcon name="grid" :size="18" />
|
:rows="4"
|
||||||
<input v-model="model" placeholder="مدل محصول" />
|
placeholder="مشخصات فنی، گارانتی، اقلام همراه یا شرایط ارائه خدمت..."
|
||||||
</div>
|
/>
|
||||||
</label>
|
</UiField>
|
||||||
<label class="field">
|
</UiCard>
|
||||||
<span>کشور سازنده</span>
|
</div>
|
||||||
<div class="control">
|
|
||||||
<UiIcon name="target" :size="18" />
|
<div class="side-column">
|
||||||
<input v-model="country" placeholder="کشور سازنده" />
|
<UiCard title="اطلاعات تکمیلی و بازرگانی" class="section-card">
|
||||||
</div>
|
<div class="side-form-stack">
|
||||||
</label>
|
<UiField label="برند یا تولیدکننده">
|
||||||
<label class="field">
|
<UiInput v-model="brand" placeholder="نام برند تجاری" />
|
||||||
<span>برچسبها</span>
|
</UiField>
|
||||||
<div class="control">
|
|
||||||
<UiIcon name="star" :size="18" />
|
<UiField label="مدل یا سری ساخت">
|
||||||
<input
|
<UiInput v-model="model" placeholder="سری و مدل" />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="کشور سازنده">
|
||||||
|
<UiInput v-model="country" placeholder="مثلاً ایران، آلمان" />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="برچسبها (تگ)" hint="برای ثبت هر برچسب Enter بزنید">
|
||||||
|
<UiInput
|
||||||
v-model="tagInput"
|
v-model="tagInput"
|
||||||
placeholder="برچسب را وارد کنید و Enter بزنید"
|
placeholder="افزودن برچسب..."
|
||||||
@keydown="onTagKey"
|
@keydown="onTagKey"
|
||||||
/>
|
/>
|
||||||
</div>
|
<div v-if="tags.length" class="tags-container">
|
||||||
<div v-if="tags.length" class="tags">
|
<UiBadge
|
||||||
<button
|
v-for="t in tags"
|
||||||
v-for="t in tags"
|
:key="t"
|
||||||
:key="t"
|
variant="neutral"
|
||||||
type="button"
|
class="tag-badge"
|
||||||
class="tag"
|
>
|
||||||
@click="removeTag(t)"
|
<span>{{ t }}</span>
|
||||||
>
|
<button type="button" class="tag-remove-btn" @click="removeTag(t)">×</button>
|
||||||
{{ t }} ×
|
</UiBadge>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</UiField>
|
||||||
</label>
|
</div>
|
||||||
</section>
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="actions">
|
<div class="form-footer-actions">
|
||||||
<button class="btn primary" type="submit" :disabled="saving">
|
<UiButton type="submit" :loading="saving">
|
||||||
<UiIcon name="check" :size="18" />
|
<template #icon>
|
||||||
|
<UiIcon name="check-plain" />
|
||||||
|
</template>
|
||||||
{{ submitLabel }}
|
{{ submitLabel }}
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="btn ghost desk-only" type="button" @click="cancel">انصراف</button>
|
<UiButton variant="ghost" type="button" :disabled="saving" @click="cancel">
|
||||||
</footer>
|
انصراف
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.product-form-page {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 1rem;
|
flex-direction: column;
|
||||||
max-width: 1100px;
|
gap: var(--sp-6);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.embedded {
|
.product-form-page.embedded {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
gap: var(--sp-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.embed-actions {
|
.embedded-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
gap: var(--sp-2);
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
}
|
||||||
.page-head {
|
|
||||||
|
.breadcrumb-link {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-link:hover {
|
||||||
|
color: var(--color-brand-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-separator {
|
||||||
|
margin: 0 var(--sp-2);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: var(--sp-6);
|
||||||
}
|
}
|
||||||
.back {
|
|
||||||
border: 1px solid var(--line);
|
.form-layout-grid {
|
||||||
background: var(--surface);
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
grid-template-columns: 2fr 1fr;
|
||||||
cursor: pointer;
|
gap: var(--sp-6);
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
.title-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.title-ico {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: var(--accent-soft);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.45rem;
|
|
||||||
}
|
|
||||||
.lead {
|
|
||||||
margin: 0.2rem 0 0;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.form {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1.4fr 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
.side {
|
|
||||||
display: grid;
|
.main-column,
|
||||||
gap: 1rem;
|
.side-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-6);
|
||||||
}
|
}
|
||||||
.card {
|
|
||||||
background: var(--surface);
|
.section-card {
|
||||||
border: 1px solid var(--line);
|
padding: var(--sp-6);
|
||||||
border-radius: 16px;
|
|
||||||
padding: 1rem 1.1rem;
|
|
||||||
display: grid;
|
|
||||||
gap: 0.85rem;
|
|
||||||
}
|
}
|
||||||
.card h2 {
|
|
||||||
margin: 0;
|
.kind-selection-row {
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.field {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.35rem;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.field em {
|
|
||||||
color: #dc2626;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
.control {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
gap: var(--sp-3);
|
||||||
border: 1px solid var(--line);
|
margin-bottom: var(--sp-4);
|
||||||
border-radius: var(--radius);
|
padding-bottom: var(--sp-4);
|
||||||
min-height: var(--control-h);
|
border-bottom: 1px solid var(--color-border);
|
||||||
padding: 0 0.75rem;
|
|
||||||
background: #fff;
|
|
||||||
color: var(--muted);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
.control input,
|
|
||||||
.control select,
|
.kind-label {
|
||||||
.control textarea {
|
font-size: var(--fs-sm);
|
||||||
border: 0;
|
font-weight: 600;
|
||||||
outline: 0;
|
color: var(--color-text-primary);
|
||||||
width: 100%;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text);
|
|
||||||
font: inherit;
|
|
||||||
}
|
}
|
||||||
.control.area {
|
|
||||||
align-items: flex-start;
|
.col-span-2 {
|
||||||
|
grid-column: span 2;
|
||||||
}
|
}
|
||||||
.control.price {
|
|
||||||
padding-inline-end: 0.35rem;
|
.price-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
}
|
}
|
||||||
.control.price :deep(input) {
|
|
||||||
border: 0;
|
.price-input-wrapper :deep(input) {
|
||||||
outline: 0;
|
flex: 1;
|
||||||
background: transparent;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
.suffix {
|
|
||||||
color: var(--muted);
|
.currency-unit-label {
|
||||||
font-size: 0.85rem;
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0.25rem 0.45rem;
|
|
||||||
background: #f8fafc;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
.row-2 {
|
|
||||||
display: grid;
|
.status-toggle-box {
|
||||||
grid-template-columns: 1fr 1fr;
|
margin-top: var(--sp-4);
|
||||||
gap: 0.75rem;
|
padding-top: var(--sp-4);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
.kind-seg {
|
|
||||||
display: grid;
|
.toggle-switch-label {
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 0.4rem;
|
|
||||||
background: #f1f5f9;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0.3rem;
|
|
||||||
}
|
|
||||||
.kind-seg button {
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0.55rem 0.6rem;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: var(--sp-2);
|
||||||
gap: 0.35rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #475569;
|
user-select: none;
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
.kind-seg button.on {
|
|
||||||
background: var(--accent);
|
.toggle-text {
|
||||||
color: #fff;
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
.status-row {
|
|
||||||
|
.side-form-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
gap: var(--sp-2);
|
||||||
gap: 0.75rem;
|
margin-top: var(--sp-2);
|
||||||
align-items: center;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
.switch {
|
|
||||||
|
.tag-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.55rem;
|
gap: var(--sp-1);
|
||||||
|
padding: var(--sp-1) var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text);
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.switch input {
|
|
||||||
position: absolute;
|
.tag-remove-btn:hover {
|
||||||
opacity: 0;
|
color: var(--color-danger-text);
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
.track {
|
|
||||||
width: 44px;
|
.form-footer-actions {
|
||||||
height: 26px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #cbd5e1;
|
|
||||||
position: relative;
|
|
||||||
transition: 0.15s ease;
|
|
||||||
}
|
|
||||||
.track::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 3px;
|
|
||||||
right: 3px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #fff;
|
|
||||||
transition: 0.15s ease;
|
|
||||||
}
|
|
||||||
.switch input:checked + .track {
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
.switch input:checked + .track::after {
|
|
||||||
right: 21px;
|
|
||||||
}
|
|
||||||
.tags {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
justify-content: flex-end;
|
||||||
gap: 0.35rem;
|
gap: var(--sp-3);
|
||||||
margin-top: 0.35rem;
|
margin-top: var(--sp-2);
|
||||||
}
|
|
||||||
.tag {
|
|
||||||
border: 0;
|
|
||||||
background: var(--accent-soft);
|
|
||||||
color: var(--accent);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.25rem 0.55rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.55rem;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.95rem;
|
|
||||||
font-weight: 700;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn.primary {
|
|
||||||
background: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
min-width: 160px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.btn.ghost {
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
.muted {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.mob-only {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.grid {
|
.form-layout-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
.row-2 {
|
|
||||||
grid-template-columns: 1fr;
|
.col-span-2 {
|
||||||
}
|
grid-column: span 1;
|
||||||
.desk-only {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.mob-only {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
.kind-desk {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.back.mob-only {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
.actions .btn.primary {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (min-width: 901px) {
|
|
||||||
.mob-only {
|
|
||||||
display: none !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,8 +5,16 @@ import axios from 'axios';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { formatClockFa, formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
import { formatClockFa, formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
||||||
import { formatMoney } from '@/lib/money';
|
import { formatMoney } from '@/lib/money';
|
||||||
import UiButton from '@/components/ui/UiButton.vue';
|
import {
|
||||||
import UiModal from '@/components/ui/UiModal.vue';
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiEmptyState,
|
||||||
|
UiInput,
|
||||||
|
UiModal,
|
||||||
|
UiPageHeader,
|
||||||
|
UiPagination,
|
||||||
|
UiSelect,
|
||||||
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import ProductFormView from '@/views/ProductFormView.vue';
|
import ProductFormView from '@/views/ProductFormView.vue';
|
||||||
import type { ImportContactsResult, ProductDto } from '@mento/shared';
|
import type { ImportContactsResult, ProductDto } from '@mento/shared';
|
||||||
@@ -73,19 +81,10 @@ const filtered = computed(() => {
|
|||||||
return list;
|
return list;
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageCount = computed(() =>
|
|
||||||
Math.max(1, Math.ceil(filtered.value.length / PAGE_SIZE)),
|
|
||||||
);
|
|
||||||
const pageItems = computed(() => {
|
const pageItems = computed(() => {
|
||||||
const start = (page.value - 1) * PAGE_SIZE;
|
const start = (page.value - 1) * PAGE_SIZE;
|
||||||
return filtered.value.slice(start, start + PAGE_SIZE);
|
return filtered.value.slice(start, start + PAGE_SIZE);
|
||||||
});
|
});
|
||||||
const pageFrom = computed(() =>
|
|
||||||
filtered.value.length ? (page.value - 1) * PAGE_SIZE + 1 : 0,
|
|
||||||
);
|
|
||||||
const pageTo = computed(() =>
|
|
||||||
Math.min(page.value * PAGE_SIZE, filtered.value.length),
|
|
||||||
);
|
|
||||||
const allPageSelected = computed(
|
const allPageSelected = computed(
|
||||||
() =>
|
() =>
|
||||||
pageItems.value.length > 0 &&
|
pageItems.value.length > 0 &&
|
||||||
@@ -262,26 +261,18 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" @click="closeMenus">
|
<section class="crm-page" @click="closeMenus">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="head-copy">
|
title="محصولات"
|
||||||
<div class="title-row">
|
lead="مدیریت محصولات، قیمتها و موجودی"
|
||||||
<span class="title-ico" aria-hidden="true">
|
>
|
||||||
<UiIcon name="box" :size="22" />
|
<template #actions>
|
||||||
</span>
|
<UiButton variant="ghost" type="button" :disabled="busy" @click="exportExcel">
|
||||||
<div>
|
|
||||||
<h1>محصولات</h1>
|
|
||||||
<p class="lead">مدیریت محصولات، قیمتها و موجودی</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="head-actions" @click.stop>
|
|
||||||
<button class="ghost-btn" type="button" :disabled="busy" @click="exportExcel">
|
|
||||||
خروجی اکسل
|
خروجی اکسل
|
||||||
</button>
|
</UiButton>
|
||||||
<button class="ghost-btn" type="button" :disabled="busy" @click="pickImportFile">
|
<UiButton variant="ghost" type="button" :disabled="busy" @click="pickImportFile">
|
||||||
ورود از اکسل
|
ورود از اکسل
|
||||||
</button>
|
</UiButton>
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -289,19 +280,19 @@ onMounted(load);
|
|||||||
hidden
|
hidden
|
||||||
@change="onImportFile"
|
@change="onImportFile"
|
||||||
/>
|
/>
|
||||||
<UiButton type="button" @click="productModal = true">
|
<UiButton variant="primary" @click="productModal = true">
|
||||||
<UiIcon name="plus" :size="16" />
|
<UiIcon name="plus" :size="16" />
|
||||||
محصول جدید
|
محصول جدید
|
||||||
</UiButton>
|
</UiButton>
|
||||||
<div class="more-wrap">
|
<div class="more-wrap" @click.stop>
|
||||||
<button
|
<UiButton
|
||||||
type="button"
|
variant="ghost"
|
||||||
class="icon-btn"
|
size="md"
|
||||||
aria-label="عملیات بیشتر"
|
aria-label="عملیات بیشتر"
|
||||||
@click="moreOpen = !moreOpen"
|
@click="moreOpen = !moreOpen"
|
||||||
>
|
>
|
||||||
<UiIcon name="dots" :size="18" />
|
<UiIcon name="dots" :size="18" />
|
||||||
</button>
|
</UiButton>
|
||||||
<div v-if="moreOpen" class="menu">
|
<div v-if="moreOpen" class="menu">
|
||||||
<button type="button" :disabled="!selected.size" @click="removeSelected">
|
<button type="button" :disabled="!selected.size" @click="removeSelected">
|
||||||
حذف انتخابشدهها
|
حذف انتخابشدهها
|
||||||
@@ -309,32 +300,35 @@ onMounted(load);
|
|||||||
<button type="button" @click="load">بروزرسانی لیست</button>
|
<button type="button" @click="load">بروزرسانی لیست</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</header>
|
</UiPageHeader>
|
||||||
|
|
||||||
<div class="toolbar" @click.stop>
|
<div class="crm-toolbar" @click.stop>
|
||||||
<label class="search">
|
<div class="search-box">
|
||||||
<UiIcon name="search" :size="18" />
|
<UiInput
|
||||||
<input
|
|
||||||
v-model="q"
|
v-model="q"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="جستجو در نام، کد یا توضیحات..."
|
placeholder="جستجو در نام، کد یا توضیحات..."
|
||||||
/>
|
>
|
||||||
</label>
|
<template #prefix>
|
||||||
<select v-model="category" class="filter-select">
|
<UiIcon name="search" :size="16" />
|
||||||
|
</template>
|
||||||
|
</UiInput>
|
||||||
|
</div>
|
||||||
|
<UiSelect v-model="category">
|
||||||
<option value="">همه دستهبندیها</option>
|
<option value="">همه دستهبندیها</option>
|
||||||
<option v-for="c in categories" :key="c" :value="c">{{ c }}</option>
|
<option v-for="c in categories" :key="c" :value="c">{{ c }}</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
<select v-model="statusFilter" class="filter-select">
|
<UiSelect v-model="statusFilter">
|
||||||
<option value="all">همه وضعیتها</option>
|
<option value="all">همه وضعیتها</option>
|
||||||
<option value="active">فعال</option>
|
<option value="active">فعال</option>
|
||||||
<option value="inactive">غیرفعال</option>
|
<option value="inactive">غیرفعال</option>
|
||||||
</select>
|
</UiSelect>
|
||||||
<div class="sort-wrap">
|
<div class="sort-wrap">
|
||||||
<button type="button" class="ghost-btn" @click="sortOpen = !sortOpen">
|
<UiButton variant="outline" @click="sortOpen = !sortOpen">
|
||||||
<UiIcon name="filter" :size="16" />
|
<UiIcon name="filter" :size="16" />
|
||||||
مرتبسازی
|
مرتبسازی
|
||||||
</button>
|
</UiButton>
|
||||||
<div v-if="sortOpen" class="menu">
|
<div v-if="sortOpen" class="menu">
|
||||||
<button type="button" :class="{ on: sort === 'name' }" @click="sort = 'name'; sortOpen = false">
|
<button type="button" :class="{ on: sort === 'name' }" @click="sort = 'name'; sortOpen = false">
|
||||||
نام
|
نام
|
||||||
@@ -356,7 +350,7 @@ onMounted(load);
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Desktop table -->
|
<!-- Desktop table -->
|
||||||
<div class="table-wrap desk">
|
<div class="table-wrap desk">
|
||||||
<table>
|
<table class="crm-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="check">
|
<th class="check">
|
||||||
@@ -398,7 +392,7 @@ onMounted(load);
|
|||||||
</td>
|
</td>
|
||||||
<td class="sku" dir="ltr">{{ p.sku || '—' }}</td>
|
<td class="sku" dir="ltr">{{ p.sku || '—' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="p.category" class="pill">{{ p.category }}</span>
|
<UiBadge v-if="p.category" variant="neutral" size="sm">{{ p.category }}</UiBadge>
|
||||||
<span v-else class="muted">—</span>
|
<span v-else class="muted">—</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="price">
|
<td class="price">
|
||||||
@@ -407,10 +401,9 @@ onMounted(load);
|
|||||||
</td>
|
</td>
|
||||||
<td class="stock">{{ p.stockQty != null ? toFaDigits(p.stockQty) : '—' }}</td>
|
<td class="stock">{{ p.stockQty != null ? toFaDigits(p.stockQty) : '—' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="status" :class="p.isActive ? 'on' : 'off'">
|
<UiBadge :variant="p.isActive ? 'success' : 'neutral'" size="sm" dot>
|
||||||
<i />
|
|
||||||
{{ p.isActive ? 'فعال' : 'غیرفعال' }}
|
{{ p.isActive ? 'فعال' : 'غیرفعال' }}
|
||||||
</span>
|
</UiBadge>
|
||||||
</td>
|
</td>
|
||||||
<td class="when">
|
<td class="when">
|
||||||
<span>{{ formatDate(p.createdAt) }}</span>
|
<span>{{ formatDate(p.createdAt) }}</span>
|
||||||
@@ -438,7 +431,12 @@ onMounted(load);
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p v-if="!pageItems.length" class="empty">محصولی یافت نشد.</p>
|
<UiEmptyState
|
||||||
|
v-if="!pageItems.length"
|
||||||
|
icon="box"
|
||||||
|
title="محصولی یافت نشد"
|
||||||
|
description="هیچ محصولی با معیارهای جستجو مطابقت ندارد."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile cards -->
|
<!-- Mobile cards -->
|
||||||
@@ -450,7 +448,7 @@ onMounted(load);
|
|||||||
<div class="mob-body">
|
<div class="mob-body">
|
||||||
<strong>{{ p.name }}</strong>
|
<strong>{{ p.name }}</strong>
|
||||||
<span class="sku" dir="ltr">{{ p.sku || '—' }}</span>
|
<span class="sku" dir="ltr">{{ p.sku || '—' }}</span>
|
||||||
<span v-if="p.category" class="pill soft">{{ p.category }}</span>
|
<UiBadge v-if="p.category" variant="neutral" size="sm">{{ p.category }}</UiBadge>
|
||||||
<span class="mob-price">
|
<span class="mob-price">
|
||||||
{{ formatPrice(p.unitPrice, p.currency) }}
|
{{ formatPrice(p.unitPrice, p.currency) }}
|
||||||
{{ currencyLabel(p.currency) }}
|
{{ currencyLabel(p.currency) }}
|
||||||
@@ -474,48 +472,25 @@ onMounted(load);
|
|||||||
<button type="button" class="danger" @click="removeProduct(p)">حذف</button>
|
<button type="button" class="danger" @click="removeProduct(p)">حذف</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="status" :class="p.isActive ? 'on' : 'off'">
|
<UiBadge :variant="p.isActive ? 'success' : 'neutral'" size="sm" dot>
|
||||||
{{ p.isActive ? 'فعال' : 'غیرفعال' }}
|
{{ p.isActive ? 'فعال' : 'غیرفعال' }}
|
||||||
</span>
|
</UiBadge>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<p v-if="!pageItems.length" class="empty">محصولی یافت نشد.</p>
|
<UiEmptyState
|
||||||
|
v-if="!pageItems.length"
|
||||||
|
icon="box"
|
||||||
|
title="محصولی یافت نشد"
|
||||||
|
description="هیچ محصولی با معیارهای جستجو مطابقت ندارد."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="filtered.length" class="pager">
|
<UiPagination
|
||||||
<p class="muted">
|
v-if="filtered.length"
|
||||||
نمایش {{ toFaDigits(pageFrom) }} تا {{ toFaDigits(pageTo) }} از
|
v-model:page="page"
|
||||||
{{ toFaDigits(filtered.length) }} مورد
|
:total-items="filtered.length"
|
||||||
</p>
|
:page-size="PAGE_SIZE"
|
||||||
<div class="pages">
|
/>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="page-btn"
|
|
||||||
:disabled="page <= 1"
|
|
||||||
@click="page -= 1"
|
|
||||||
>
|
|
||||||
‹
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-for="n in pageCount"
|
|
||||||
:key="n"
|
|
||||||
type="button"
|
|
||||||
class="page-btn"
|
|
||||||
:class="{ on: page === n }"
|
|
||||||
@click="page = n"
|
|
||||||
>
|
|
||||||
{{ toFaDigits(n) }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="page-btn"
|
|
||||||
:disabled="page >= pageCount"
|
|
||||||
@click="page += 1"
|
|
||||||
>
|
|
||||||
›
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UiModal :open="productModal" title="محصول جدید" @close="productModal = false">
|
<UiModal :open="productModal" title="محصول جدید" @close="productModal = false">
|
||||||
@@ -530,126 +505,15 @@ onMounted(load);
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.more-wrap,
|
||||||
display: grid;
|
.sort-wrap,
|
||||||
gap: 1rem;
|
.row-more {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-bottom: 4.5rem;
|
|
||||||
}
|
}
|
||||||
.page-head {
|
|
||||||
display: flex;
|
.search-box {
|
||||||
flex-wrap: wrap;
|
flex: 1 1 240px;
|
||||||
justify-content: space-between;
|
min-width: 200px;
|
||||||
gap: 1rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.title-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.title-ico {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: var(--accent-soft);
|
|
||||||
color: var(--accent);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.55rem;
|
|
||||||
}
|
|
||||||
.lead {
|
|
||||||
margin: 0.2rem 0 0;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.92rem;
|
|
||||||
}
|
|
||||||
.head-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.45rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
height: var(--control-h);
|
|
||||||
padding: 0 0.95rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.btn.primary {
|
|
||||||
background: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.icon-btn,
|
|
||||||
.ghost-btn {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.icon-btn {
|
|
||||||
width: var(--control-h);
|
|
||||||
height: var(--control-h);
|
|
||||||
}
|
|
||||||
.ghost-btn {
|
|
||||||
min-height: var(--control-h);
|
|
||||||
height: var(--control-h);
|
|
||||||
padding: 0 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.55rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.search {
|
|
||||||
flex: 1 1 220px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.45rem;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: var(--surface);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
height: var(--control-h);
|
|
||||||
padding: 0 0.75rem;
|
|
||||||
color: var(--muted);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.search input {
|
|
||||||
border: 0;
|
|
||||||
outline: 0;
|
|
||||||
background: transparent;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
.filter-select {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: var(--surface);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
height: var(--control-h);
|
|
||||||
padding: 0 0.75rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: var(--text);
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
}
|
||||||
.more-wrap,
|
.more-wrap,
|
||||||
.sort-wrap,
|
.sort-wrap,
|
||||||
@@ -664,7 +528,7 @@ h1 {
|
|||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.1);
|
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.1);
|
||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -678,7 +542,7 @@ h1 {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding: 0.55rem 0.7rem;
|
padding: 0.55rem 0.7rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
@@ -693,7 +557,7 @@ h1 {
|
|||||||
.table-wrap {
|
.table-wrap {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
@@ -729,7 +593,7 @@ tr:last-child td {
|
|||||||
.thumb {
|
.thumb {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
@@ -817,7 +681,7 @@ tr:last-child td {
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
}
|
}
|
||||||
.mob-body {
|
.mob-body {
|
||||||
|
|||||||
@@ -767,11 +767,10 @@ watch(
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.profile {
|
.profile {
|
||||||
max-width: 760px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0 auto;
|
margin: 0;
|
||||||
padding-bottom: 5.6rem;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
@@ -806,7 +805,7 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-brand {
|
.nav-brand {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
letter-spacing: -0.04em;
|
letter-spacing: -0.04em;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
@@ -817,7 +816,7 @@ watch(
|
|||||||
height: 38px;
|
height: 38px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
@@ -842,7 +841,7 @@ watch(
|
|||||||
min-width: 12.75rem;
|
min-width: 12.75rem;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.16);
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.16);
|
||||||
padding: 0.4rem;
|
padding: 0.4rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -862,7 +861,7 @@ watch(
|
|||||||
background: none;
|
background: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0.68rem 0.8rem;
|
padding: 0.68rem 0.8rem;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius);
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -1000,7 +999,7 @@ h1 {
|
|||||||
.qa-card {
|
.qa-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.85rem 0.4rem 0.75rem;
|
padding: 0.85rem 0.4rem 0.75rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
@@ -1015,7 +1014,7 @@ h1 {
|
|||||||
.qa-ico {
|
.qa-ico {
|
||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
@@ -1028,7 +1027,7 @@ h1 {
|
|||||||
.smart {
|
.smart {
|
||||||
background: linear-gradient(135deg, #ecfeff, #f5f3ff);
|
background: linear-gradient(135deg, #ecfeff, #f5f3ff);
|
||||||
border: 1px solid #e0e7ff;
|
border: 1px solid #e0e7ff;
|
||||||
border-radius: 20px;
|
border-radius: var(--radius);
|
||||||
padding: 0.95rem 1rem 0.85rem;
|
padding: 0.95rem 1rem 0.85rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
@@ -1060,7 +1059,7 @@ h1 {
|
|||||||
.smart-cta {
|
.smart-cta {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: #ccfbf1;
|
background: #ccfbf1;
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -1117,7 +1116,7 @@ h1 {
|
|||||||
|
|
||||||
.kpi {
|
.kpi {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.7rem 0.65rem 0.75rem;
|
padding: 0.7rem 0.65rem 0.75rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.04);
|
||||||
@@ -1151,7 +1150,7 @@ h1 {
|
|||||||
.kpi-ico {
|
.kpi-ico {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 9px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
margin-bottom: 0.15rem;
|
margin-bottom: 0.15rem;
|
||||||
@@ -1272,7 +1271,7 @@ h1 {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.15rem;
|
gap: 0.15rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
padding: 0.85rem 0.95rem;
|
padding: 0.85rem 0.95rem;
|
||||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
@@ -1293,8 +1292,8 @@ h1 {
|
|||||||
.info-form {
|
.info-form {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.4rem 1rem 1rem;
|
padding: 0.4rem 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
@@ -1374,7 +1373,7 @@ h1 {
|
|||||||
bottom: 0.6rem;
|
bottom: 0.6rem;
|
||||||
margin-top: 0.4rem;
|
margin-top: 0.4rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 22px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
|
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
@@ -1477,7 +1476,7 @@ h1 {
|
|||||||
.qa-card {
|
.qa-card {
|
||||||
padding: 0.7rem 0.2rem 0.6rem;
|
padding: 0.7rem 0.2rem 0.6rem;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.qa-ico {
|
.qa-ico {
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ onMounted(load);
|
|||||||
.page {
|
.page {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
max-width: 640px;
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -237,7 +237,7 @@ onMounted(load);
|
|||||||
.doc-ico {
|
.doc-ico {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: #ecfdf5;
|
background: #ecfdf5;
|
||||||
@@ -296,7 +296,7 @@ h1 {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ function backToForm() {
|
|||||||
display: grid;
|
display: grid;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 28px;
|
border-radius: var(--radius);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ function backToForm() {
|
|||||||
|
|
||||||
.mobile-brand strong {
|
.mobile-brand strong {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-brand span {
|
.mobile-brand span {
|
||||||
@@ -413,7 +413,7 @@ function backToForm() {
|
|||||||
.title-ico {
|
.title-ico {
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 50% 38%, #0d9488 0 18%, transparent 19%),
|
radial-gradient(circle at 50% 38%, #0d9488 0 18%, transparent 19%),
|
||||||
@@ -428,7 +428,7 @@ function backToForm() {
|
|||||||
inset-inline-end: 6px;
|
inset-inline-end: 6px;
|
||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
color: #0d9488;
|
color: #0d9488;
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@ h1 {
|
|||||||
.input-wrap input,
|
.input-wrap input,
|
||||||
.form > .grid-2 .field > input {
|
.form > .grid-2 .field > input {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
padding: 0.8rem 0.95rem;
|
padding: 0.8rem 0.95rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -500,7 +500,7 @@ h1 {
|
|||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: #64748b;
|
background: #64748b;
|
||||||
border-radius: 4px;
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ico.user {
|
.ico.user {
|
||||||
@@ -514,7 +514,7 @@ h1 {
|
|||||||
background: linear-gradient(#64748b, #64748b);
|
background: linear-gradient(#64748b, #64748b);
|
||||||
mask: linear-gradient(#000 0 0);
|
mask: linear-gradient(#000 0 0);
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
border-radius: 3px;
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ico.lock,
|
.ico.lock,
|
||||||
@@ -547,7 +547,7 @@ h1 {
|
|||||||
.submit {
|
.submit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.9rem 1.1rem;
|
padding: 0.9rem 1.1rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-cta {
|
.login-cta {
|
||||||
@@ -565,7 +565,7 @@ h1 {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: min(100%, 280px);
|
min-width: min(100%, 280px);
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--line));
|
border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--line));
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -584,7 +584,7 @@ h1 {
|
|||||||
.dev-hint {
|
.dev-hint {
|
||||||
margin: 0.75rem 0 1rem;
|
margin: 0.75rem 0 1rem;
|
||||||
padding: 0.65rem 0.8rem;
|
padding: 0.65rem 0.8rem;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
|
|||||||
+698
-709
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,12 @@ import {
|
|||||||
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
||||||
import MoneyInput from '@/components/MoneyInput.vue';
|
import MoneyInput from '@/components/MoneyInput.vue';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
import {
|
||||||
|
UiPageHeader,
|
||||||
|
UiPagination,
|
||||||
|
UiChart,
|
||||||
|
type UiChartOption,
|
||||||
|
} from '@/components/ui';
|
||||||
import type {
|
import type {
|
||||||
CreateSalesGoalRequest,
|
CreateSalesGoalRequest,
|
||||||
OrgUserDto,
|
OrgUserDto,
|
||||||
@@ -29,7 +35,6 @@ type StatusFilter = 'all' | SalesGoalStatus;
|
|||||||
type MobTab = 'goals' | 'forecast';
|
type MobTab = 'goals' | 'forecast';
|
||||||
type MobScreen = 'list' | 'form';
|
type MobScreen = 'list' | 'form';
|
||||||
|
|
||||||
const PAGE_SIZES = [10, 20, 50];
|
|
||||||
const GOAL_TYPES: { value: SalesGoalType; label: string; icon: 'target' | 'users' | 'chart' | 'briefcase' | 'check' | 'star' }[] = [
|
const GOAL_TYPES: { value: SalesGoalType; label: string; icon: 'target' | 'users' | 'chart' | 'briefcase' | 'check' | 'star' }[] = [
|
||||||
{ value: 'sales', label: 'فروش', icon: 'target' },
|
{ value: 'sales', label: 'فروش', icon: 'target' },
|
||||||
{ value: 'customer', label: 'مشتری', icon: 'users' },
|
{ value: 'customer', label: 'مشتری', icon: 'users' },
|
||||||
@@ -103,9 +108,6 @@ const pageItems = computed(() => {
|
|||||||
const pageFrom = computed(() =>
|
const pageFrom = computed(() =>
|
||||||
filteredGoals.value.length ? (page.value - 1) * pageSize.value + 1 : 0,
|
filteredGoals.value.length ? (page.value - 1) * pageSize.value + 1 : 0,
|
||||||
);
|
);
|
||||||
const pageTo = computed(() =>
|
|
||||||
Math.min(page.value * pageSize.value, filteredGoals.value.length),
|
|
||||||
);
|
|
||||||
|
|
||||||
const formTitle = computed(() =>
|
const formTitle = computed(() =>
|
||||||
editingId.value ? 'ویرایش هدف' : 'افزودن هدف جدید',
|
editingId.value ? 'ویرایش هدف' : 'افزودن هدف جدید',
|
||||||
@@ -133,6 +135,51 @@ function formatMoney(amount: number) {
|
|||||||
return formatMoneyLib(amount, currency.value);
|
return formatMoneyLib(amount, currency.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const forecastChartOption = computed<UiChartOption>(() => {
|
||||||
|
const f = forecast.value;
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const item = Array.isArray(params) ? params[0] : params;
|
||||||
|
if (!item) return '';
|
||||||
|
return `${item.name}: <strong>${formatMoney(item.value)}</strong>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: { left: '3%', right: '4%', bottom: '8%', top: '8%', containLabel: true },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: ['هدف دوره', 'تحققیافته', 'کاریز باز', 'پیشبینی'],
|
||||||
|
axisLine: { lineStyle: { color: '#cbd5e1' } },
|
||||||
|
axisLabel: { color: '#64748b', fontFamily: 'IRANSans' },
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLine: { show: false },
|
||||||
|
splitLine: { lineStyle: { color: '#f1f5f9', type: 'dashed' } },
|
||||||
|
axisLabel: {
|
||||||
|
color: '#64748b',
|
||||||
|
fontFamily: 'IRANSans',
|
||||||
|
formatter: (v: number) => toFaDigits(Math.round(v).toLocaleString('en-US')),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'مبلغ',
|
||||||
|
type: 'bar',
|
||||||
|
barWidth: '40%',
|
||||||
|
data: [
|
||||||
|
{ value: f?.targetAmount || 0, itemStyle: { color: '#3b82f6', borderRadius: [6, 6, 0, 0] } },
|
||||||
|
{ value: f?.wonAmount || 0, itemStyle: { color: '#10b981', borderRadius: [6, 6, 0, 0] } },
|
||||||
|
{ value: f?.openPipelineAmount || 0, itemStyle: { color: '#f59e0b', borderRadius: [6, 6, 0, 0] } },
|
||||||
|
{ value: f?.forecastAmount || 0, itemStyle: { color: '#14b8a6', borderRadius: [6, 6, 0, 0] } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
function formatValue(amount: number, unit: 'money' | 'count', type?: string) {
|
function formatValue(amount: number, unit: 'money' | 'count', type?: string) {
|
||||||
if (unit === 'count' || type === 'customer' || type === 'deal' || type === 'activity') {
|
if (unit === 'count' || type === 'customer' || type === 'deal' || type === 'activity') {
|
||||||
return `${toFaDigits(Math.round(amount))} ${type === 'customer' ? 'مشتری' : type === 'deal' ? 'معامله' : type === 'activity' ? 'فعالیت' : 'مورد'}`;
|
return `${toFaDigits(Math.round(amount))} ${type === 'customer' ? 'مشتری' : type === 'deal' ? 'معامله' : type === 'activity' ? 'فعالیت' : 'مورد'}`;
|
||||||
@@ -418,15 +465,10 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="title-row">
|
title="هدف و پیشبینی"
|
||||||
<span class="title-ico"><UiIcon name="target" :size="22" /></span>
|
lead="اهداف ماهانه را تنظیم و پیشبینیهای خود را مشاهده نمایید"
|
||||||
<div>
|
/>
|
||||||
<h1>هدف و پیشبینی</h1>
|
|
||||||
<p class="lead">اهداف ماهانه را تنظیم و پیشبینیهای خود را مشاهده نمایید</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="mob-tabs mob-only">
|
<div class="mob-tabs mob-only">
|
||||||
<button type="button" :class="{ on: mobTab === 'goals' }" @click="mobTab = 'goals'">لیست اهداف</button>
|
<button type="button" :class="{ on: mobTab === 'goals' }" @click="mobTab = 'goals'">لیست اهداف</button>
|
||||||
@@ -666,37 +708,18 @@ onMounted(() => {
|
|||||||
<div class="sum-row payable"><span>پیشبینی درآمد</span><strong>{{ formatMoney(forecast?.forecastAmount || 0) }}</strong></div>
|
<div class="sum-row payable"><span>پیشبینی درآمد</span><strong>{{ formatMoney(forecast?.forecastAmount || 0) }}</strong></div>
|
||||||
<div class="sum-row"><span>مشتریان جدید</span><strong>{{ toFaDigits(forecast?.newCustomersCount || 0) }}</strong></div>
|
<div class="sum-row"><span>مشتریان جدید</span><strong>{{ toFaDigits(forecast?.newCustomersCount || 0) }}</strong></div>
|
||||||
<div class="sum-row"><span>معاملات جدید</span><strong>{{ toFaDigits(forecast?.newDealsCount || 0) }}</strong></div>
|
<div class="sum-row"><span>معاملات جدید</span><strong>{{ toFaDigits(forecast?.newDealsCount || 0) }}</strong></div>
|
||||||
|
<UiChart :option="forecastChartOption" height="220px" style="margin-top: 1rem;" />
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="filteredGoals.length && mobTab !== 'forecast'" class="pager desk-only">
|
<UiPagination
|
||||||
<div class="pager-meta">
|
v-if="filteredGoals.length && mobTab !== 'forecast'"
|
||||||
<p class="muted">
|
:page="page"
|
||||||
نمایش {{ toFaDigits(pageFrom) }} تا {{ toFaDigits(pageTo) }} از
|
:total-pages="pageCount"
|
||||||
{{ toFaDigits(filteredGoals.length) }} مورد
|
:total-items="filteredGoals.length"
|
||||||
</p>
|
:page-size="pageSize"
|
||||||
<label class="page-size">
|
@update:page="page = $event"
|
||||||
نمایش در هر صفحه:
|
/>
|
||||||
<select v-model.number="pageSize">
|
|
||||||
<option v-for="n in PAGE_SIZES" :key="n" :value="n">{{ toFaDigits(n) }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="pages">
|
|
||||||
<button type="button" class="page-btn" :disabled="page <= 1" @click="page -= 1">‹</button>
|
|
||||||
<button
|
|
||||||
v-for="n in pageCount"
|
|
||||||
:key="n"
|
|
||||||
type="button"
|
|
||||||
class="page-btn"
|
|
||||||
:class="{ on: page === n }"
|
|
||||||
@click="page = n"
|
|
||||||
>
|
|
||||||
{{ toFaDigits(n) }}
|
|
||||||
</button>
|
|
||||||
<button type="button" class="page-btn" :disabled="page >= pageCount" @click="page += 1">›</button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -716,27 +739,28 @@ onMounted(() => {
|
|||||||
--teal-soft: #f0fdfa;
|
--teal-soft: #f0fdfa;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding-bottom: 5.5rem;
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.page-head { display: flex; justify-content: space-between; gap: 1rem; }
|
.page-head { display: flex; justify-content: space-between; gap: 1rem; }
|
||||||
.title-row { display: flex; gap: 0.75rem; align-items: flex-start; }
|
.title-row { display: flex; gap: 0.75rem; align-items: flex-start; }
|
||||||
.title-ico, .side-ico, .doc-badge, .kpi-ico {
|
.title-ico, .side-ico, .doc-badge, .kpi-ico {
|
||||||
width: 42px; height: 42px; border-radius: 12px; display: grid; place-items: center;
|
width: 42px; height: 42px; border-radius: var(--radius); display: grid; place-items: center;
|
||||||
background: var(--teal-soft); color: var(--teal); flex-shrink: 0; position: relative;
|
background: var(--teal-soft); color: var(--teal); flex-shrink: 0; position: relative;
|
||||||
}
|
}
|
||||||
.doc-badge .plus {
|
.doc-badge .plus {
|
||||||
position: absolute; bottom: 4px; left: 4px; background: var(--teal); color: #fff;
|
position: absolute; bottom: 4px; left: 4px; background: var(--teal); color: #fff;
|
||||||
border-radius: 4px; padding: 1px;
|
border-radius: var(--radius); padding: 1px;
|
||||||
}
|
}
|
||||||
h1 { margin: 0; font-size: 1.4rem; font-weight: 800; }
|
h1 { margin: 0; font-size: 1.4rem; font-weight: 600; }
|
||||||
.lead, .mob-head-copy p { margin: 0.2rem 0 0; color: var(--muted); font-size: 0.88rem; }
|
.lead, .mob-head-copy p { margin: 0.2rem 0 0; color: var(--muted); font-size: 0.88rem; }
|
||||||
|
|
||||||
.kpi-row {
|
.kpi-row {
|
||||||
display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.75rem;
|
display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.75rem;
|
||||||
}
|
}
|
||||||
.kpi-card {
|
.kpi-card {
|
||||||
background: #fff; border: 1px solid #eef2f7; border-radius: 16px; padding: 0.9rem;
|
background: #fff; border: 1px solid #eef2f7; border-radius: var(--radius); padding: 0.9rem;
|
||||||
display: grid; grid-template-columns: auto 1fr auto; gap: 0.7rem; align-items: center;
|
display: grid; grid-template-columns: auto 1fr auto; gap: 0.7rem; align-items: center;
|
||||||
box-shadow: 0 8px 20px rgba(15,23,42,.04);
|
box-shadow: 0 8px 20px rgba(15,23,42,.04);
|
||||||
}
|
}
|
||||||
@@ -748,7 +772,7 @@ h1 { margin: 0; font-size: 1.4rem; font-weight: 800; }
|
|||||||
content: ''; position: absolute; inset: 7px; border-radius: 50%; background: #fff;
|
content: ''; position: absolute; inset: 7px; border-radius: 50%; background: #fff;
|
||||||
}
|
}
|
||||||
.ring span {
|
.ring span {
|
||||||
position: relative; z-index: 1; font-size: 0.78rem; font-weight: 800; color: var(--teal);
|
position: relative; z-index: 1; font-size: 0.78rem; font-weight: 600; color: var(--teal);
|
||||||
}
|
}
|
||||||
.ring.sm { width: 56px; height: 56px; }
|
.ring.sm { width: 56px; height: 56px; }
|
||||||
.ring.sm::before { inset: 6px; }
|
.ring.sm::before { inset: 6px; }
|
||||||
@@ -762,7 +786,7 @@ h1 { margin: 0; font-size: 1.4rem; font-weight: 800; }
|
|||||||
display: grid; grid-template-columns: minmax(280px, 320px) minmax(0, 1fr); gap: 1rem; align-items: start;
|
display: grid; grid-template-columns: minmax(280px, 320px) minmax(0, 1fr); gap: 1rem; align-items: start;
|
||||||
}
|
}
|
||||||
.sidebar, .main, .card, .table-wrap, .goal-card, .forecast-summary {
|
.sidebar, .main, .card, .table-wrap, .goal-card, .forecast-summary {
|
||||||
background: #fff; border: 1px solid #eef2f7; border-radius: 16px;
|
background: #fff; border: 1px solid #eef2f7; border-radius: var(--radius);
|
||||||
box-shadow: 0 8px 20px rgba(15,23,42,.04);
|
box-shadow: 0 8px 20px rgba(15,23,42,.04);
|
||||||
}
|
}
|
||||||
.sidebar { padding: 1rem; display: grid; gap: 0.85rem; position: sticky; top: 0.75rem; }
|
.sidebar { padding: 1rem; display: grid; gap: 0.85rem; position: sticky; top: 0.75rem; }
|
||||||
@@ -776,36 +800,42 @@ h1 { margin: 0; font-size: 1.4rem; font-weight: 800; }
|
|||||||
.control :deep(.ico) {
|
.control :deep(.ico) {
|
||||||
position: absolute; inset-inline-start: 0.7rem; color: #94a3b8; pointer-events: none; z-index: 1;
|
position: absolute; inset-inline-start: 0.7rem; color: #94a3b8; pointer-events: none; z-index: 1;
|
||||||
}
|
}
|
||||||
.control input, .control select, .control textarea, .search input, .mob-filter select {
|
.control input, .control select, .control textarea, .mob-filter select {
|
||||||
width: 100%; border: 1px solid #e2e8f0; border-radius: var(--radius); min-height: var(--control-h);
|
width: 100%; border: 1px solid var(--line); border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h);
|
||||||
padding: 0 0.8rem; background: #fff; font: inherit; color: inherit; box-sizing: border-box;
|
padding: 0 0.8rem; background: #fff; font: inherit; font-size: var(--fs-sm); color: inherit; box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.control input, .control select, .control textarea { padding-inline-start: 2.2rem; }
|
.control input, .control select, .control textarea { padding-inline-start: 2.2rem; }
|
||||||
.control.area textarea { min-height: 84px; resize: vertical; }
|
.control.area textarea { min-height: 84px; resize: vertical; }
|
||||||
.row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
|
.row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
|
||||||
.forecast-block {
|
.forecast-block {
|
||||||
border-top: 1px dashed #e2e8f0; padding-top: 0.75rem; display: grid; gap: 0.65rem;
|
border-top: 1px dashed var(--line); padding-top: 0.75rem; display: grid; gap: 0.65rem;
|
||||||
}
|
}
|
||||||
.forecast-block h3 {
|
.forecast-block h3 {
|
||||||
margin: 0; display: flex; align-items: center; gap: 0.35rem; font-size: 0.95rem; color: var(--teal);
|
margin: 0; display: flex; align-items: center; gap: 0.35rem; font-size: 0.95rem; color: var(--accent);
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
border: 0; border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h);
|
border: 0; border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h);
|
||||||
padding: 0 1rem; font-weight: 800; box-sizing: border-box;
|
padding: 0 0.85rem; font-weight: 600; font-size: var(--fs-sm); box-sizing: border-box;
|
||||||
display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; cursor: pointer;
|
display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; cursor: pointer;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
.btn.primary { background: var(--teal); color: #fff; }
|
.btn.primary { background: var(--accent); color: #fff; }
|
||||||
.btn.ghost { background: #fff; border: 1px solid #e2e8f0; color: #334155; }
|
.btn.ghost { background: #fff; border: 1px solid var(--line); color: var(--ink-secondary); }
|
||||||
.btn.full { width: 100%; }
|
.btn.full { width: 100%; }
|
||||||
|
|
||||||
.main { padding: 0.9rem; display: grid; gap: 0.85rem; min-width: 0; }
|
.main { padding: 0.9rem; display: grid; gap: 0.85rem; min-width: 0; }
|
||||||
.toolbar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
.toolbar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
||||||
.search {
|
.search {
|
||||||
flex: 1 1 180px; display: flex; align-items: center; gap: 0.4rem;
|
flex: 1 1 180px; max-width: 380px; display: flex; align-items: center; gap: 0.4rem;
|
||||||
border: 1px solid #e2e8f0; border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h);
|
border: 1px solid var(--line); border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h);
|
||||||
padding: 0 0.7rem; color: #94a3b8; background: #fff; box-sizing: border-box;
|
padding: 0 0.75rem; color: var(--muted); background: #fff; box-sizing: border-box;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
}
|
}
|
||||||
.search input { border: 0; outline: 0; background: transparent; color: var(--text); }
|
.search:focus-within {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||||
|
}
|
||||||
|
.search input { border: 0; outline: 0; background: transparent; color: var(--ink); width: 100%; font-size: var(--fs-sm); padding: 0; }
|
||||||
.status-tabs { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
.status-tabs { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||||
.status-tabs button {
|
.status-tabs button {
|
||||||
border: 0; background: #f1f5f9; color: #64748b; border-radius: 999px; padding: 0.4rem 0.75rem;
|
border: 0; background: #f1f5f9; color: #64748b; border-radius: 999px; padding: 0.4rem 0.75rem;
|
||||||
@@ -838,18 +868,18 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; white-space: nowrap;
|
|||||||
.status.needs_attention { background: #ffedd5; color: #c2410c; }
|
.status.needs_attention { background: #ffedd5; color: #c2410c; }
|
||||||
.ops { position: relative; white-space: nowrap; }
|
.ops { position: relative; white-space: nowrap; }
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
width: 34px; height: 34px; border-radius: 10px; border: 1px solid #e2e8f0; background: #fff;
|
width: 34px; height: 34px; border-radius: var(--radius); border: 1px solid #e2e8f0; background: #fff;
|
||||||
display: inline-flex; align-items: center; justify-content: center; cursor: pointer; color: #64748b;
|
display: inline-flex; align-items: center; justify-content: center; cursor: pointer; color: #64748b;
|
||||||
}
|
}
|
||||||
.icon-btn.sm { width: 30px; height: 30px; }
|
.icon-btn.sm { width: 30px; height: 30px; }
|
||||||
.menu {
|
.menu {
|
||||||
position: absolute; top: calc(100% + 4px); left: 0; z-index: 20; min-width: 120px;
|
position: absolute; top: calc(100% + 4px); left: 0; z-index: 20; min-width: 120px;
|
||||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 10px; box-shadow: 0 10px 24px rgba(15,23,42,.12);
|
background: #fff; border: 1px solid #e2e8f0; border-radius: var(--radius); box-shadow: 0 10px 24px rgba(15,23,42,.12);
|
||||||
padding: 0.3rem; display: grid;
|
padding: 0.3rem; display: grid;
|
||||||
}
|
}
|
||||||
.menu.left { right: 0; left: auto; }
|
.menu.left { right: 0; left: auto; }
|
||||||
.menu button {
|
.menu button {
|
||||||
border: 0; background: transparent; text-align: right; padding: 0.5rem 0.65rem; border-radius: 8px;
|
border: 0; background: transparent; text-align: right; padding: 0.5rem 0.65rem; border-radius: var(--radius);
|
||||||
cursor: pointer; font: inherit;
|
cursor: pointer; font: inherit;
|
||||||
}
|
}
|
||||||
.menu button:hover { background: var(--teal-soft); color: var(--teal); }
|
.menu button:hover { background: var(--teal-soft); color: var(--teal); }
|
||||||
@@ -858,10 +888,10 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; white-space: nowrap;
|
|||||||
.pager { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem; align-items: center; }
|
.pager { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.75rem; align-items: center; }
|
||||||
.pager-meta { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
|
.pager-meta { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
|
||||||
.page-size { display: inline-flex; gap: 0.35rem; align-items: center; color: var(--muted); font-size: 0.85rem; }
|
.page-size { display: inline-flex; gap: 0.35rem; align-items: center; color: var(--muted); font-size: 0.85rem; }
|
||||||
.page-size select { border: 1px solid #e2e8f0; border-radius: 10px; padding: 0.25rem 0.45rem; }
|
.page-size select { border: 1px solid #e2e8f0; border-radius: var(--radius); padding: 0.25rem 0.45rem; }
|
||||||
.pages { display: flex; gap: 0.25rem; }
|
.pages { display: flex; gap: 0.25rem; }
|
||||||
.page-btn {
|
.page-btn {
|
||||||
min-width: 32px; height: 32px; border-radius: 10px; border: 1px solid #e2e8f0; background: #fff; cursor: pointer;
|
min-width: 32px; height: 32px; border-radius: var(--radius); border: 1px solid #e2e8f0; background: #fff; cursor: pointer;
|
||||||
}
|
}
|
||||||
.page-btn.on { background: var(--teal); color: #fff; border-color: transparent; }
|
.page-btn.on { background: var(--teal); color: #fff; border-color: transparent; }
|
||||||
|
|
||||||
@@ -883,12 +913,12 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; white-space: nowrap;
|
|||||||
.goal-body { display: grid; grid-template-columns: 1fr auto; gap: 0.65rem; align-items: center; }
|
.goal-body { display: grid; grid-template-columns: 1fr auto; gap: 0.65rem; align-items: center; }
|
||||||
.goal-body strong { display: block; }
|
.goal-body strong { display: block; }
|
||||||
.goal-body .muted, .goal-body small { display: block; font-size: 0.8rem; }
|
.goal-body .muted, .goal-body small { display: block; font-size: 0.8rem; }
|
||||||
.goal-body p { margin: 0.25rem 0; font-weight: 800; font-size: 0.9rem; }
|
.goal-body p { margin: 0.25rem 0; font-weight: 600; font-size: 0.9rem; }
|
||||||
.sum-row {
|
.sum-row {
|
||||||
display: flex; justify-content: space-between; gap: 0.5rem; padding: 0.45rem 0;
|
display: flex; justify-content: space-between; gap: 0.5rem; padding: 0.45rem 0;
|
||||||
border-bottom: 1px solid #f1f5f9; font-size: 0.9rem;
|
border-bottom: 1px solid #f1f5f9; font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.sum-row.payable { color: var(--teal); font-weight: 800; border-bottom: 0; }
|
.sum-row.payable { color: var(--teal); font-weight: 600; border-bottom: 0; }
|
||||||
.forecast-summary { padding: 1rem; }
|
.forecast-summary { padding: 1rem; }
|
||||||
.mob-bottom {
|
.mob-bottom {
|
||||||
position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
|
position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
|
||||||
@@ -897,7 +927,7 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; white-space: nowrap;
|
|||||||
background: #fff; border-top: 1px solid #eef2f7; box-shadow: 0 -8px 24px rgba(15,23,42,.06);
|
background: #fff; border-top: 1px solid #eef2f7; box-shadow: 0 -8px 24px rgba(15,23,42,.06);
|
||||||
}
|
}
|
||||||
.fab {
|
.fab {
|
||||||
border: 0; border-radius: 10px; min-height: 36px; background: var(--teal); color: #fff; font-weight: 800;
|
border: 0; border-radius: var(--radius); min-height: 36px; background: var(--teal); color: #fff; font-weight: 600;
|
||||||
padding: 0 1rem; cursor: pointer;
|
padding: 0 1rem; cursor: pointer;
|
||||||
}
|
}
|
||||||
.side-link {
|
.side-link {
|
||||||
@@ -929,8 +959,8 @@ th { background: #f8fafc; color: #64748b; font-weight: 700; white-space: nowrap;
|
|||||||
background: #f1f5f9; border-radius: var(--radius);
|
background: #f1f5f9; border-radius: var(--radius);
|
||||||
}
|
}
|
||||||
.mob-tabs button {
|
.mob-tabs button {
|
||||||
border: 0; border-radius: 8px; min-height: var(--control-h); height: var(--control-h); background: transparent; color: #64748b;
|
border: 0; border-radius: var(--radius); min-height: var(--control-h); height: var(--control-h); background: transparent; color: #64748b;
|
||||||
font-weight: 800; cursor: pointer;
|
font-weight: 600; cursor: pointer;
|
||||||
}
|
}
|
||||||
.mob-tabs button.on { background: var(--teal); color: #fff; }
|
.mob-tabs button.on { background: var(--teal); color: #fff; }
|
||||||
.mob-filter { flex: 1; }
|
.mob-filter { flex: 1; }
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ import type {
|
|||||||
UpdateTahesabConnectionRequest,
|
UpdateTahesabConnectionRequest,
|
||||||
} from '@mento/shared';
|
} from '@mento/shared';
|
||||||
import { brand } from '@mento/shared';
|
import { brand } from '@mento/shared';
|
||||||
|
import {
|
||||||
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiField,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
UiSelect,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@@ -221,29 +231,27 @@ async function removeConn(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchTahesab(mode: 'filter' | 'range' = 'filter') {
|
async function searchTahesab(mode: 'range' | 'filter') {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
ok.value = '';
|
ok.value = '';
|
||||||
syncResults.value = [];
|
|
||||||
if (!sync.connectionId) {
|
if (!sync.connectionId) {
|
||||||
error.value = 'ابتدا اتصال را انتخاب کنید.';
|
error.value = 'ابتدا یک اتصال انتخاب کنید.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
syncBusy.value = true;
|
||||||
try {
|
try {
|
||||||
syncBusy.value = true;
|
const body: Record<string, unknown> = { connectionId: sync.connectionId };
|
||||||
const body: Record<string, unknown> = {
|
|
||||||
connectionId: sync.connectionId,
|
|
||||||
};
|
|
||||||
if (mode === 'range') {
|
if (mode === 'range') {
|
||||||
const fromRaw = String(sync.fromCode ?? '').trim();
|
const fromRaw = String(sync.fromCode ?? '').trim();
|
||||||
const toRaw = String(sync.toCode ?? '').trim();
|
const toRaw = String(sync.toCode ?? '').trim();
|
||||||
if (!fromRaw && !toRaw) {
|
if (!fromRaw && !toRaw) {
|
||||||
body.listAll = true;
|
body.fromCode = 1;
|
||||||
|
body.toCode = 5000;
|
||||||
} else {
|
} else {
|
||||||
const fromCode = Number(fromRaw);
|
const fromCode = fromRaw ? Number(fromRaw) : 1;
|
||||||
const toCode = Number(toRaw);
|
const toCode = toRaw ? Number(toRaw) : fromCode + 1999;
|
||||||
if (!Number.isFinite(fromCode) || !Number.isFinite(toCode)) {
|
if (Number.isNaN(fromCode) || Number.isNaN(toCode) || fromCode < 1 || toCode < fromCode) {
|
||||||
error.value = 'بازهٔ کد را درست وارد کنید، یا هر دو را خالی بگذارید تا همه نمایش داده شوند.';
|
error.value = 'بازه کد مشتری نامعتبر است.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
body.fromCode = fromCode;
|
body.fromCode = fromCode;
|
||||||
@@ -328,22 +336,27 @@ async function importBulk(mode: 'results' | 'range') {
|
|||||||
if (!fromRaw && !toRaw) {
|
if (!fromRaw && !toRaw) {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
`همهٔ مشتریان تهحساب وارد ${brand.nameFa} میشوند (قبلاً لینکشدهها رد میشوند). ممکن است کمی طول بکشد. ادامه؟`,
|
`کل مشتریان از کد ۱ تا ۵۰۰۰ از تهحساب بهعنوان شخص وارد ${brand.nameFa} میشوند. ادامه؟`,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
body.listAll = true;
|
body.fromCode = 1;
|
||||||
|
body.toCode = 5000;
|
||||||
} else {
|
} else {
|
||||||
const fromCode = Number(fromRaw);
|
const fromCode = fromRaw ? Number(fromRaw) : 1;
|
||||||
const toCode = Number(toRaw);
|
const toCode = toRaw ? Number(toRaw) : fromCode + 1999;
|
||||||
if (!Number.isFinite(fromCode) || !Number.isFinite(toCode)) {
|
if (Number.isNaN(fromCode) || Number.isNaN(toCode) || fromCode < 1 || toCode < fromCode) {
|
||||||
error.value = 'بازهٔ کد را درست وارد کنید، یا هر دو را خالی بگذارید تا همه وارد شوند.';
|
error.value = 'بازه نامعتبر است.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (toCode - fromCode > 2000) {
|
||||||
|
error.value = 'حداکثر طول بازه در هر درخواست ۲۰۰۰ کد است.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
`همهٔ مشتریان تهحساب با کد ${fromCode} تا ${toCode} وارد ${brand.nameFa} میشوند (قبلاً لینکشدهها رد میشوند). ادامه؟`,
|
`مشتریان با کد ${fromCode} تا ${toCode} از تهحساب وارد ${brand.nameFa} میشوند. ادامه؟`,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -372,482 +385,469 @@ onMounted(load);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="crm-page tahesab-page" dir="rtl">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="head-copy">
|
title="اتصال نرمافزار مالی تهحساب"
|
||||||
<h1>اتصال تهحساب</h1>
|
description="همگامسازی اشخاص، ماندهحسابها و اسناد مالی بین تهحساب و نرمافزار مدیریت ارتباط با مشتریان"
|
||||||
<p class="lead">
|
/>
|
||||||
هر نسخه/دیتابیس تهحساب یک اتصال جدا است (آدرس وبسرویس + توکن + نام DB).
|
|
||||||
اگر فقط یک اتصال دارید، همان بهعنوان پیشفرض استفاده میشود.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<div v-if="ok" class="crm-alert crm-alert-success">{{ ok }}</div>
|
||||||
<p v-if="ok" class="ok">{{ ok }}</p>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="items.some((r) => !r.allowInsecureTls && isTlsError(r.lastError))"
|
v-if="items.some((r) => !r.allowInsecureTls && isTlsError(r.lastError))"
|
||||||
class="banner warn"
|
class="crm-alert crm-alert-warning"
|
||||||
>
|
>
|
||||||
اتصال شما بهخاطر گواهی SSL خودامضا قطع است. روی همان کارت اتصال،
|
اتصال شما بهخاطر گواهی SSL خودامضا قطع است. روی همان کارت اتصال،
|
||||||
<strong>فعالسازی SSL خودامضا</strong> را بزنید؛ بعد دوباره «نمایش بازه» را امتحان کنید.
|
<strong>فعالسازی SSL خودامضا</strong> را بزنید؛ بعد دوباره «نمایش بازه» را امتحان کنید.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!loading && items.length" class="list">
|
<UiCard v-if="!loading && items.length" title="اتصالات تعریفشده" class="section-card">
|
||||||
<article v-for="row in items" :key="row.id" class="card row">
|
<div class="connections-list">
|
||||||
<div class="row-main">
|
<div v-for="row in items" :key="row.id" class="connection-row">
|
||||||
<strong>{{ row.label }}</strong>
|
<div class="connection-info">
|
||||||
<span class="muted">{{ row.baseUrl }}</span>
|
<div class="connection-title-row">
|
||||||
<span class="meta">
|
<strong class="connection-label">{{ row.label }}</strong>
|
||||||
DB: {{ row.dbName }} · توکن: {{ row.tokenMasked }}
|
<UiBadge v-if="row.isDefault" variant="primary">پیشفرض</UiBadge>
|
||||||
<template v-if="row.isDefault"> · پیشفرض</template>
|
<UiBadge :variant="row.enabled ? 'success' : 'neutral'">
|
||||||
<template v-if="!row.enabled"> · غیرفعال</template>
|
{{ row.enabled ? 'فعال' : 'غیرفعال' }}
|
||||||
<template v-if="row.allowInsecureTls"> · SSL خودامضا مجاز</template>
|
</UiBadge>
|
||||||
</span>
|
<UiBadge v-if="row.allowInsecureTls" variant="warning">SSL خودامضا</UiBadge>
|
||||||
<span
|
</div>
|
||||||
v-if="row.lastHealthAt"
|
<p class="connection-url font-mono">{{ row.baseUrl }}</p>
|
||||||
class="health"
|
<p class="connection-meta">
|
||||||
:class="row.lastHealthOk ? 'ok' : 'bad'"
|
<span>بانک: {{ row.dbName }}</span>
|
||||||
>
|
<span class="meta-dot">·</span>
|
||||||
آخرین تست:
|
<span>توکن: {{ row.tokenMasked }}</span>
|
||||||
{{ row.lastHealthOk ? 'موفق' : 'ناموفق' }}
|
</p>
|
||||||
<template v-if="row.lastError"> — {{ row.lastError }}</template>
|
<div
|
||||||
</span>
|
v-if="row.lastHealthAt"
|
||||||
|
class="health-status"
|
||||||
|
:class="row.lastHealthOk ? 'ok' : 'bad'"
|
||||||
|
>
|
||||||
|
<span class="health-indicator"></span>
|
||||||
|
<span>آخرین وضعیت: {{ row.lastHealthOk ? 'موفق' : 'ناموفق' }}</span>
|
||||||
|
<span v-if="row.lastError"> — {{ row.lastError }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="connection-actions">
|
||||||
|
<UiButton
|
||||||
|
v-if="!row.allowInsecureTls && isTlsError(row.lastError)"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
@click="enableInsecureTls(row.id)"
|
||||||
|
>
|
||||||
|
فعالسازی SSL خودامضا
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="ghost" size="sm" @click="startEdit(row)">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="pencil" />
|
||||||
|
</template>
|
||||||
|
ویرایش
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="ghost" size="sm" @click="testConn(row.id)">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="refresh" />
|
||||||
|
</template>
|
||||||
|
تست اتصال
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="danger" size="sm" @click="removeConn(row.id)">
|
||||||
|
<template #icon>
|
||||||
|
<UiIcon name="trash" />
|
||||||
|
</template>
|
||||||
|
حذف
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-actions">
|
</div>
|
||||||
<button
|
</UiCard>
|
||||||
v-if="!row.allowInsecureTls && isTlsError(row.lastError)"
|
|
||||||
type="button"
|
<UiCard :title="editingId ? 'ویرایش اتصال تهحساب' : 'افزودن اتصال جدید تهحساب'" class="section-card">
|
||||||
class="btn primary"
|
<form class="crm-form" @submit.prevent="save">
|
||||||
@click="enableInsecureTls(row.id)"
|
<div class="crm-form-grid-2">
|
||||||
>
|
<UiField label="عنوان اتصال" required>
|
||||||
فعالسازی SSL خودامضا
|
<UiInput v-model="form.label" required maxlength="120" placeholder="مثلاً شعبه مرکزی، دیتابیس مالی" />
|
||||||
</button>
|
</UiField>
|
||||||
<button type="button" class="btn ghost" @click="startEdit(row)">ویرایش</button>
|
|
||||||
<button type="button" class="btn ghost" @click="testConn(row.id)">تست</button>
|
<UiField label="آدرس وبسرویس (Base URL)" required hint="آدرس سرور وبسرویس تهحساب">
|
||||||
<button type="button" class="btn danger" @click="removeConn(row.id)">حذف</button>
|
<UiInput
|
||||||
|
v-model="form.baseUrl"
|
||||||
|
required
|
||||||
|
dir="ltr"
|
||||||
|
placeholder="https://IP:8081 یا https://api.tahesab.ir"
|
||||||
|
@blur="onBaseUrlBlur"
|
||||||
|
/>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="نام دیتابیس (DBName)" required>
|
||||||
|
<UiInput v-model="form.dbName" required dir="ltr" placeholder="DB یا TahesabDB" />
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="توکن دسترسی (Bearer Token)" :required="!editingId" :hint="editingId ? 'خالی بگذارید تا توکن فعلی تغییر نکند.' : 'توکن احراز هویت وبسرویس'">
|
||||||
|
<UiInput
|
||||||
|
v-model="form.token"
|
||||||
|
:required="!editingId"
|
||||||
|
type="password"
|
||||||
|
dir="ltr"
|
||||||
|
autocomplete="new-password"
|
||||||
|
placeholder="توکن امنیتی..."
|
||||||
|
/>
|
||||||
|
</UiField>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="card" @submit.prevent="save">
|
<div class="checkbox-group">
|
||||||
<h2>{{ editingId ? 'ویرایش اتصال' : 'افزودن اتصال' }}</h2>
|
<label class="custom-check">
|
||||||
<div class="field">
|
<input v-model="form.isDefault" type="checkbox" />
|
||||||
<label>برچسب</label>
|
<span>اتصال پیشفرض سازمان</span>
|
||||||
<input v-model="form.label" required maxlength="120" placeholder="مثلاً شعبه بازار" />
|
</label>
|
||||||
</div>
|
<label class="custom-check">
|
||||||
<div class="field">
|
<input v-model="form.enabled" type="checkbox" />
|
||||||
<label>آدرس وبسرویس (base URL)</label>
|
<span>اتصال فعال باشد</span>
|
||||||
<input
|
</label>
|
||||||
v-model="form.baseUrl"
|
<label class="custom-check highlight">
|
||||||
required
|
<input v-model="form.allowInsecureTls" type="checkbox" />
|
||||||
dir="ltr"
|
<span>پذیرش گواهی SSL خودامضا (برای IP محلی یا پورت داخلی بدون گواهی رسمی)</span>
|
||||||
placeholder="https://api-test.tahesab.ir یا https://IP:8081"
|
</label>
|
||||||
@blur="onBaseUrlBlur"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>نام دیتابیس (DBname)</label>
|
|
||||||
<input v-model="form.dbName" required dir="ltr" placeholder="DB یا TahesabDB" />
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>
|
|
||||||
توکن
|
|
||||||
<span v-if="editingId" class="muted">(خالی بگذارید تا عوض نشود)</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="form.token"
|
|
||||||
:required="!editingId"
|
|
||||||
type="password"
|
|
||||||
dir="ltr"
|
|
||||||
autocomplete="new-password"
|
|
||||||
placeholder="Bearer token"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<label class="check">
|
|
||||||
<input v-model="form.isDefault" type="checkbox" />
|
|
||||||
اتصال پیشفرض
|
|
||||||
</label>
|
|
||||||
<label class="check">
|
|
||||||
<input v-model="form.enabled" type="checkbox" />
|
|
||||||
فعال
|
|
||||||
</label>
|
|
||||||
<label class="check highlight">
|
|
||||||
<input v-model="form.allowInsecureTls" type="checkbox" />
|
|
||||||
پذیرش گواهی SSL خودامضا (برای IP داخلی / https بدون گواهی معتبر — معمولاً لازم است)
|
|
||||||
</label>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button class="btn primary" type="submit" :disabled="saving">
|
|
||||||
{{ saving ? 'در حال ذخیره…' : editingId ? 'بهروزرسانی' : 'افزودن' }}
|
|
||||||
</button>
|
|
||||||
<button v-if="editingId" type="button" class="btn ghost" @click="resetForm">انصراف</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<section class="card sync">
|
<div class="form-actions-row">
|
||||||
<h2>وارد کردن مشتریان از تهحساب</h2>
|
<UiButton type="submit" :loading="saving">
|
||||||
<p class="muted">
|
{{ editingId ? 'بهروزرسانی اتصال' : 'افزودن اتصال' }}
|
||||||
فقط «افزودن اتصال» مشتریها را نمیآورد. باید با بازهٔ کد یا جستجو آنها را از تهحساب
|
</UiButton>
|
||||||
<strong>وارد</strong> کنید تا در «اشخاص و شرکتها» دیده شوند. جزئیات مانده/اسناد بعد از لینک، در پروفایل شخص → تب دارایی است.
|
<UiButton v-if="editingId" variant="ghost" type="button" @click="resetForm">
|
||||||
|
انصراف
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard title="وارد کردن و همگامسازی مشتریان از تهحساب" class="section-card">
|
||||||
|
<p class="sync-desc">
|
||||||
|
با استفاده از این بخش میتوانید اشخاص و طرفحسابهای تهحساب را به عنوان مخاطب وارد نرمافزار کرده و سوابق مالی آنها را پیوند دهید.
|
||||||
</p>
|
</p>
|
||||||
<div class="field">
|
|
||||||
<label>اتصال</label>
|
|
||||||
<select v-model="sync.connectionId">
|
|
||||||
<option disabled value="">انتخاب اتصال</option>
|
|
||||||
<option v-for="row in items" :key="row.id" :value="row.id">
|
|
||||||
{{ row.label }} ({{ row.dbName }})
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="subhead">۱) وارد کردن دستهای با بازهٔ کد مشتری</h3>
|
<div class="crm-form">
|
||||||
<p class="muted small">
|
<UiField label="اتصال مورد نظر جهت همگامسازی" required>
|
||||||
اگر «از» و «تا» خالی باشند، مشتریان با کد ۱ تا ۵۰۰۰ از تهحساب لیست میشوند.
|
<UiSelect v-model="sync.connectionId">
|
||||||
برای کدهای بالاتر بازه را دستی بگذارید (حداکثر ۲۰۰۰ کد در هر درخواست محدود).
|
<option disabled value="">انتخاب اتصال</option>
|
||||||
</p>
|
<option v-for="row in items" :key="row.id" :value="row.id">
|
||||||
<div class="sync-grid range">
|
{{ row.label }} ({{ row.dbName }})
|
||||||
<input v-model="sync.fromCode" type="number" min="1" dir="ltr" placeholder="از کد (خالی = همه)" />
|
</option>
|
||||||
<input v-model="sync.toCode" type="number" min="1" dir="ltr" placeholder="تا کد (خالی = همه)" />
|
</UiSelect>
|
||||||
</div>
|
</UiField>
|
||||||
<div class="sync-actions">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn ghost"
|
|
||||||
:disabled="!sync.connectionId || syncBusy"
|
|
||||||
@click="searchTahesab('range')"
|
|
||||||
>
|
|
||||||
نمایش بازه
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn primary"
|
|
||||||
:disabled="!sync.connectionId || syncBusy"
|
|
||||||
@click="importBulk('range')"
|
|
||||||
>
|
|
||||||
وارد کردن کل بازه
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="subhead">۲) جستجوی تکی</h3>
|
<div class="sync-sub-section">
|
||||||
<div class="sync-grid">
|
<h4 class="sync-subhead">۱) وارد کردن دستهای با بازه کد مشتری</h4>
|
||||||
<input v-model="sync.phone" dir="ltr" placeholder="موبایل" />
|
<p class="sync-sub-desc">
|
||||||
<input v-model="sync.name" placeholder="نام" />
|
در صورت خالی بودن «از» و «تا»، مشتریان کد ۱ تا ۵۰۰۰ بررسی میشوند (حداکثر ۲۰۰۰ کد در هر درخواست).
|
||||||
<input v-model="sync.moshtariCode" type="number" dir="ltr" placeholder="کد مشتری" />
|
</p>
|
||||||
</div>
|
<div class="crm-form-grid-2">
|
||||||
<div class="sync-actions">
|
<UiInput v-model="sync.fromCode" type="number" min="1" dir="ltr" placeholder="از کد (مثلاً ۱)" />
|
||||||
<button
|
<UiInput v-model="sync.toCode" type="number" min="1" dir="ltr" placeholder="تا کد (مثلاً ۵۰۰)" />
|
||||||
type="button"
|
</div>
|
||||||
class="btn ghost"
|
<div class="sync-buttons">
|
||||||
:disabled="!sync.connectionId || syncBusy"
|
<UiButton
|
||||||
@click="searchTahesab('filter')"
|
variant="secondary"
|
||||||
>
|
:disabled="!sync.connectionId || syncBusy"
|
||||||
جستجو
|
@click="searchTahesab('range')"
|
||||||
</button>
|
>
|
||||||
<button
|
نمایش بازه
|
||||||
type="button"
|
</UiButton>
|
||||||
class="btn primary"
|
<UiButton
|
||||||
:disabled="!sync.connectionId || syncBusy || !pendingImportCodes().length"
|
:disabled="!sync.connectionId || syncBusy"
|
||||||
@click="importBulk('results')"
|
@click="importBulk('range')"
|
||||||
>
|
>
|
||||||
وارد کردن نتایج جدید ({{ pendingImportCodes().length }})
|
وارد کردن کل بازه
|
||||||
</button>
|
</UiButton>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul v-if="syncResults.length" class="sync-results">
|
<div class="sync-sub-section">
|
||||||
<li v-for="r in syncResults" :key="String(r.moshtariCode)">
|
<h4 class="sync-subhead">۲) جستجوی تکی یا فیلتر مشخصات</h4>
|
||||||
<span>
|
<div class="crm-form-grid-3">
|
||||||
{{ r.moshtariCode }} — {{ r.name || 'بدون نام' }}
|
<UiInput v-model="sync.phone" dir="ltr" placeholder="موبایل یا تلفن..." />
|
||||||
<span class="muted">{{ r.tel }}</span>
|
<UiInput v-model="sync.name" placeholder="نام یا عنوان مشتری..." />
|
||||||
<span v-if="r.alreadyLinked" class="linked">لینکشده{{ r.personName ? ` · ${r.personName}` : '' }}</span>
|
<UiInput v-model="sync.moshtariCode" type="number" dir="ltr" placeholder="کد مشتری تهحساب..." />
|
||||||
</span>
|
</div>
|
||||||
<button
|
<div class="sync-buttons">
|
||||||
v-if="!r.alreadyLinked"
|
<UiButton
|
||||||
type="button"
|
variant="secondary"
|
||||||
class="btn"
|
:disabled="!sync.connectionId || syncBusy"
|
||||||
:disabled="!r.moshtariCode || syncBusy"
|
@click="searchTahesab('filter')"
|
||||||
@click="importOne(r.moshtariCode!)"
|
>
|
||||||
>
|
جستجو در تهحساب
|
||||||
وارد کردن
|
</UiButton>
|
||||||
</button>
|
<UiButton
|
||||||
<RouterLink
|
:disabled="!sync.connectionId || syncBusy || !pendingImportCodes().length"
|
||||||
v-else-if="r.personId"
|
@click="importBulk('results')"
|
||||||
class="btn ghost"
|
>
|
||||||
:to="{ name: 'person-detail', params: { id: r.personId } }"
|
وارد کردن نتایج جدید ({{ pendingImportCodes().length }})
|
||||||
>
|
</UiButton>
|
||||||
مشاهده شخص
|
</div>
|
||||||
</RouterLink>
|
</div>
|
||||||
</li>
|
|
||||||
</ul>
|
<div v-if="syncResults.length" class="sync-results-box">
|
||||||
</section>
|
<h4 class="sync-results-title">نتایج جستجو ({{ syncResults.length }})</h4>
|
||||||
|
<div class="crm-table-container">
|
||||||
|
<table class="crm-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>کد مشتری</th>
|
||||||
|
<th>نام و نام خانوادگی</th>
|
||||||
|
<th>تلفن</th>
|
||||||
|
<th>وضعیت لینک</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="r in syncResults" :key="String(r.moshtariCode)">
|
||||||
|
<td class="font-mono">{{ r.moshtariCode }}</td>
|
||||||
|
<td class="font-bold">{{ r.name || 'بدون نام' }}</td>
|
||||||
|
<td class="font-mono" dir="ltr">{{ r.tel }}</td>
|
||||||
|
<td>
|
||||||
|
<UiBadge v-if="r.alreadyLinked" variant="success">
|
||||||
|
لینک شده{{ r.personName ? ` (${r.personName})` : '' }}
|
||||||
|
</UiBadge>
|
||||||
|
<UiBadge v-else variant="neutral">آماده ورود</UiBadge>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<UiButton
|
||||||
|
v-if="!r.alreadyLinked"
|
||||||
|
size="sm"
|
||||||
|
:disabled="!r.moshtariCode || syncBusy"
|
||||||
|
@click="importOne(r.moshtariCode!)"
|
||||||
|
>
|
||||||
|
وارد کردن
|
||||||
|
</UiButton>
|
||||||
|
<RouterLink
|
||||||
|
v-else-if="r.personId"
|
||||||
|
:to="{ name: 'person-detail', params: { id: r.personId } }"
|
||||||
|
>
|
||||||
|
<UiButton variant="ghost" size="sm">
|
||||||
|
مشاهده شخص
|
||||||
|
</UiButton>
|
||||||
|
</RouterLink>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.tahesab-page {
|
||||||
display: grid;
|
display: flex;
|
||||||
gap: 1rem;
|
flex-direction: column;
|
||||||
max-width: 720px;
|
gap: var(--sp-6);
|
||||||
min-width: 0;
|
width: 100%;
|
||||||
overflow-x: clip;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-head {
|
.section-card {
|
||||||
|
padding: var(--sp-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
padding: var(--sp-4) var(--sp-5);
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
transition: border-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.head-copy {
|
.connection-row:hover {
|
||||||
min-width: 0;
|
border-color: var(--color-brand-300, #5eead4);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.connection-info {
|
||||||
margin: 0 0 0.3rem;
|
|
||||||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lead,
|
|
||||||
.muted {
|
|
||||||
margin: 0;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.75rem;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 18px;
|
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
|
||||||
padding: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
align-items: start;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-main {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.25rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health.ok {
|
|
||||||
color: #15803d;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health.bad {
|
|
||||||
color: #b91c1c;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
gap: 0.35rem;
|
gap: var(--sp-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.connection-title-row {
|
||||||
display: grid;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='text'],
|
|
||||||
input:not([type]),
|
|
||||||
input[type='password'],
|
|
||||||
input[type='url'],
|
|
||||||
input[type='number'] {
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.7rem;
|
|
||||||
background: #fff;
|
|
||||||
color: #0f172a;
|
|
||||||
font: inherit;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.7rem;
|
|
||||||
background: #fff;
|
|
||||||
color: #0f172a;
|
|
||||||
font: inherit;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.45rem;
|
gap: var(--sp-2);
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check.highlight {
|
|
||||||
padding: 0.65rem 0.75rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #fff7ed;
|
|
||||||
border: 1px solid #fdba74;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #9a3412;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner.warn {
|
|
||||||
padding: 0.85rem 1rem;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: #fff7ed;
|
|
||||||
border: 1px solid #fdba74;
|
|
||||||
color: #9a3412;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.connection-label {
|
||||||
|
font-size: var(--fs-md);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-url {
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
direction: ltr;
|
||||||
|
text-align: right;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-meta {
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-dot {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-status {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: var(--sp-2);
|
||||||
min-height: var(--control-h);
|
font-size: var(--fs-xs);
|
||||||
padding: 0 0.9rem;
|
margin-top: var(--sp-1);
|
||||||
border-radius: var(--radius);
|
}
|
||||||
font-weight: 700;
|
|
||||||
border: 1px solid transparent;
|
.health-indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-status.ok {
|
||||||
|
color: var(--color-success-text, #15803d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-status.ok .health-indicator {
|
||||||
|
background: var(--color-success-text, #15803d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-status.bad {
|
||||||
|
color: var(--color-danger-text, #b91c1c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-status.bad .health-indicator {
|
||||||
|
background: var(--color-danger-text, #b91c1c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
margin: var(--sp-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-check {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.custom-check.highlight {
|
||||||
background: #14b8a6;
|
padding: var(--sp-2) var(--sp-3);
|
||||||
color: #fff;
|
border-radius: var(--radius-md);
|
||||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
background: var(--color-warning-bg, #fffbeb);
|
||||||
}
|
border: 1px solid var(--color-warning-border, #fef3c7);
|
||||||
|
color: var(--color-warning-text, #92400e);
|
||||||
.btn.ghost {
|
|
||||||
background: #fff;
|
|
||||||
color: #0f766e;
|
|
||||||
border-color: #99f6e4;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.danger {
|
|
||||||
background: #fff;
|
|
||||||
color: #b91c1c;
|
|
||||||
border-color: #fecaca;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
margin: 0;
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ok {
|
|
||||||
margin: 0;
|
|
||||||
color: #0f766e;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-grid {
|
.form-actions-row {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-grid.range {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sync-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
gap: var(--sp-2);
|
||||||
gap: 0.5rem;
|
margin-top: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subhead {
|
.sync-desc {
|
||||||
margin: 0.35rem 0 0;
|
font-size: var(--fs-sm);
|
||||||
font-size: 0.95rem;
|
color: var(--color-text-secondary);
|
||||||
color: #0f766e;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted.small {
|
.sync-sub-section {
|
||||||
font-size: 0.85rem;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding: var(--sp-4);
|
||||||
|
background: var(--color-surface-soft);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.linked {
|
.sync-subhead {
|
||||||
display: inline-block;
|
font-size: var(--fs-sm);
|
||||||
margin-inline-start: 0.35rem;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #047857;
|
color: var(--color-text-primary);
|
||||||
}
|
|
||||||
|
|
||||||
.sync-results {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
|
||||||
display: grid;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-results li {
|
.sync-sub-desc {
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
gap: var(--sp-2);
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
.sync-results-box {
|
||||||
.row {
|
display: flex;
|
||||||
grid-template-columns: 1fr;
|
flex-direction: column;
|
||||||
}
|
gap: var(--sp-3);
|
||||||
|
margin-top: var(--sp-2);
|
||||||
|
}
|
||||||
|
|
||||||
.sync-grid {
|
.sync-results-title {
|
||||||
grid-template-columns: 1fr;
|
font-size: var(--fs-sm);
|
||||||
}
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.row-actions,
|
.font-mono {
|
||||||
.form-actions,
|
font-family: var(--font-mono, monospace);
|
||||||
.sync-results li {
|
}
|
||||||
|
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.connection-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connection-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ import axios from 'axios';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { canViewAllRecords } from '@/lib/roles';
|
import { canViewAllRecords } from '@/lib/roles';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import {
|
||||||
|
UiBadge,
|
||||||
|
UiButton,
|
||||||
|
UiCard,
|
||||||
|
UiField,
|
||||||
|
UiInput,
|
||||||
|
UiPageHeader,
|
||||||
|
UiSelect,
|
||||||
|
UiTextarea,
|
||||||
|
} from '@/components/ui';
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import type {
|
import type {
|
||||||
CompanyDto,
|
CompanyDto,
|
||||||
CreateTicketRequest,
|
CreateTicketRequest,
|
||||||
@@ -24,6 +35,7 @@ const id = computed(() =>
|
|||||||
typeof route.params.id === 'string' ? route.params.id : '',
|
typeof route.params.id === 'string' ? route.params.id : '',
|
||||||
);
|
);
|
||||||
const isNew = computed(() => route.name === 'ticket-new');
|
const isNew = computed(() => route.name === 'ticket-new');
|
||||||
|
const pageTitle = computed(() => (isNew.value ? 'تیکت جدید' : number.value || 'تیکت'));
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@@ -33,8 +45,8 @@ const error = ref('');
|
|||||||
const number = ref('');
|
const number = ref('');
|
||||||
const subject = ref('');
|
const subject = ref('');
|
||||||
const description = ref('');
|
const description = ref('');
|
||||||
const status = ref<TicketStatus>('open');
|
const status = ref<string>('open');
|
||||||
const priority = ref<TicketPriority>('normal');
|
const priority = ref<string>('normal');
|
||||||
const personId = ref('');
|
const personId = ref('');
|
||||||
const companyId = ref('');
|
const companyId = ref('');
|
||||||
const assigneeId = ref('');
|
const assigneeId = ref('');
|
||||||
@@ -138,8 +150,8 @@ async function load() {
|
|||||||
number.value = data.number;
|
number.value = data.number;
|
||||||
subject.value = data.subject;
|
subject.value = data.subject;
|
||||||
description.value = data.description ?? '';
|
description.value = data.description ?? '';
|
||||||
status.value = data.status as TicketStatus;
|
status.value = data.status;
|
||||||
priority.value = data.priority as TicketPriority;
|
priority.value = data.priority;
|
||||||
personId.value = data.person?.id ?? '';
|
personId.value = data.person?.id ?? '';
|
||||||
companyId.value = data.company?.id ?? '';
|
companyId.value = data.company?.id ?? '';
|
||||||
assigneeId.value = data.assignee?.id ?? '';
|
assigneeId.value = data.assignee?.id ?? '';
|
||||||
@@ -168,8 +180,8 @@ function payload(): CreateTicketRequest {
|
|||||||
return {
|
return {
|
||||||
subject: subject.value.trim(),
|
subject: subject.value.trim(),
|
||||||
description: description.value || undefined,
|
description: description.value || undefined,
|
||||||
status: status.value,
|
status: status.value as TicketStatus,
|
||||||
priority: priority.value,
|
priority: priority.value as TicketPriority,
|
||||||
personId: personId.value || null,
|
personId: personId.value || null,
|
||||||
companyId: companyId.value || null,
|
companyId: companyId.value || null,
|
||||||
assigneeId: assigneeId.value || null,
|
assigneeId: assigneeId.value || null,
|
||||||
@@ -266,21 +278,49 @@ async function remove() {
|
|||||||
await router.push({ name: 'tickets' });
|
await router.push({ name: 'tickets' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
void router.push({ name: 'tickets' });
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page">
|
<section class="crm-page narrow">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="head-copy">
|
:title="pageTitle"
|
||||||
<p class="crumb">
|
:back-to="{ name: 'tickets' }"
|
||||||
|
>
|
||||||
|
<template #lead>
|
||||||
|
<span class="crumb">
|
||||||
<RouterLink :to="{ name: 'tickets' }">تیکتها</RouterLink>
|
<RouterLink :to="{ name: 'tickets' }">تیکتها</RouterLink>
|
||||||
<span>›</span>
|
<span>›</span>
|
||||||
<strong>{{ isNew ? 'تیکت جدید' : number || 'تیکت' }}</strong>
|
<strong>{{ pageTitle }}</strong>
|
||||||
</p>
|
</span>
|
||||||
<h1>{{ isNew ? 'تیکت جدید' : number || 'تیکت' }}</h1>
|
</template>
|
||||||
</div>
|
<template #actions>
|
||||||
</header>
|
<UiButton
|
||||||
|
variant="primary"
|
||||||
|
:disabled="saving"
|
||||||
|
:loading="saving"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
<UiIcon name="check" :size="16" />
|
||||||
|
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
||||||
|
</UiButton>
|
||||||
|
<UiButton variant="ghost" :disabled="saving" @click="cancel">
|
||||||
|
انصراف
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
v-if="!isNew"
|
||||||
|
variant="danger"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="remove"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</UiButton>
|
||||||
|
</template>
|
||||||
|
</UiPageHeader>
|
||||||
|
|
||||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
||||||
|
|
||||||
@@ -290,137 +330,163 @@ onMounted(load);
|
|||||||
class="sla-box"
|
class="sla-box"
|
||||||
:class="{ bad: ticket.slaBreached }"
|
:class="{ bad: ticket.slaBreached }"
|
||||||
>
|
>
|
||||||
<template v-if="ticket.slaBreached">مهلت پاسخ اول گذشته است.</template>
|
<UiIcon name="clock" :size="18" />
|
||||||
<template v-else-if="ticket.firstRespondedAt">
|
<div>
|
||||||
پاسخ اول ثبت شد ({{ formatWhen(ticket.firstRespondedAt) }}).
|
<template v-if="ticket.slaBreached">
|
||||||
</template>
|
مهلت پاسخ اول گذشته است.
|
||||||
<template v-else>
|
</template>
|
||||||
مهلت پاسخ اول: {{ formatWhen(ticket.slaDueAt) }}
|
<template v-else-if="ticket.firstRespondedAt">
|
||||||
</template>
|
پاسخ اول ثبت شد ({{ formatWhen(ticket.firstRespondedAt) }}).
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
مهلت پاسخ اول: {{ formatWhen(ticket.slaDueAt) }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<form class="form" @submit.prevent="save">
|
<form class="crm-form" @submit.prevent="save">
|
||||||
<div class="grid">
|
<UiCard>
|
||||||
<div class="field wide">
|
<div class="crm-form-grid-2">
|
||||||
<label>موضوع</label>
|
<div class="crm-col-span-full">
|
||||||
<input v-model="subject" required maxlength="200" />
|
<UiField label="موضوع" required for-id="subject">
|
||||||
</div>
|
<UiInput
|
||||||
<div class="field">
|
id="subject"
|
||||||
<label>وضعیت</label>
|
v-model="subject"
|
||||||
<select v-model="status">
|
required
|
||||||
<option
|
maxlength="200"
|
||||||
v-for="(label, key) in statusLabel"
|
placeholder="موضوع تیکت را وارد کنید…"
|
||||||
:key="key"
|
/>
|
||||||
:value="key"
|
</UiField>
|
||||||
>
|
</div>
|
||||||
{{ label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>اولویت</label>
|
|
||||||
<select v-model="priority">
|
|
||||||
<option
|
|
||||||
v-for="(label, key) in priorityLabel"
|
|
||||||
:key="key"
|
|
||||||
:value="key"
|
|
||||||
>
|
|
||||||
{{ label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>شخص</label>
|
|
||||||
<select v-model="personId">
|
|
||||||
<option value="">بدون شخص</option>
|
|
||||||
<option v-for="p in people" :key="p.id" :value="p.id">
|
|
||||||
{{ p.fullName }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>شرکت</label>
|
|
||||||
<select v-model="companyId">
|
|
||||||
<option value="">بدون شرکت</option>
|
|
||||||
<option v-for="c in companies" :key="c.id" :value="c.id">
|
|
||||||
{{ c.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div v-if="canAssignOthers" class="field">
|
|
||||||
<label>مسئول</label>
|
|
||||||
<select v-model="assigneeId">
|
|
||||||
<option value="">بدون مسئول</option>
|
|
||||||
<option v-for="u in users" :key="u.id" :value="u.id">
|
|
||||||
{{ u.fullName }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field wide">
|
|
||||||
<label>{{ isNew ? 'شرح اولیه' : 'شرح' }}</label>
|
|
||||||
<textarea v-model="description" rows="4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="isNew && faqHints.length" class="faq-hints">
|
<UiField label="وضعیت" for-id="status">
|
||||||
<p class="faq-title">شاید این FAQ کمک کند:</p>
|
<UiSelect id="status" v-model="status">
|
||||||
<details v-for="f in faqHints" :key="f.id">
|
<option
|
||||||
|
v-for="(label, key) in statusLabel"
|
||||||
|
:key="key"
|
||||||
|
:value="key"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
</UiSelect>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="اولویت" for-id="priority">
|
||||||
|
<UiSelect id="priority" v-model="priority">
|
||||||
|
<option
|
||||||
|
v-for="(label, key) in priorityLabel"
|
||||||
|
:key="key"
|
||||||
|
:value="key"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</option>
|
||||||
|
</UiSelect>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="شخص مرتبط" for-id="personId">
|
||||||
|
<UiSelect id="personId" v-model="personId">
|
||||||
|
<option value="">بدون شخص</option>
|
||||||
|
<option v-for="p in people" :key="p.id" :value="p.id">
|
||||||
|
{{ p.fullName }}
|
||||||
|
</option>
|
||||||
|
</UiSelect>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField label="شرکت مرتبط" for-id="companyId">
|
||||||
|
<UiSelect id="companyId" v-model="companyId">
|
||||||
|
<option value="">بدون شرکت</option>
|
||||||
|
<option v-for="c in companies" :key="c.id" :value="c.id">
|
||||||
|
{{ c.name }}
|
||||||
|
</option>
|
||||||
|
</UiSelect>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<UiField v-if="canAssignOthers" label="مسئول" for-id="assigneeId">
|
||||||
|
<UiSelect id="assigneeId" v-model="assigneeId">
|
||||||
|
<option value="">بدون مسئول</option>
|
||||||
|
<option v-for="u in users" :key="u.id" :value="u.id">
|
||||||
|
{{ u.fullName }}
|
||||||
|
</option>
|
||||||
|
</UiSelect>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<div class="crm-col-span-full">
|
||||||
|
<UiField :label="isNew ? 'شرح اولیه' : 'شرح'" for-id="description">
|
||||||
|
<UiTextarea
|
||||||
|
id="description"
|
||||||
|
v-model="description"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="جزئیات و توضیحات تیکت را بنویسید…"
|
||||||
|
/>
|
||||||
|
</UiField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard v-if="isNew && faqHints.length" class="faq-hints-card" padding="sm">
|
||||||
|
<p class="faq-title">
|
||||||
|
<UiIcon name="lightbulb" :size="16" />
|
||||||
|
شاید این مقالات راهنما (FAQ) کمک کند:
|
||||||
|
</p>
|
||||||
|
<details v-for="f in faqHints" :key="f.id" class="faq-item">
|
||||||
<summary>{{ f.question }}</summary>
|
<summary>{{ f.question }}</summary>
|
||||||
<p>{{ f.answer }}</p>
|
<p>{{ f.answer }}</p>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</UiCard>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error-msg">{{ error }}</p>
|
||||||
<div class="form-actions">
|
|
||||||
<button class="btn" type="submit" :disabled="saving">
|
|
||||||
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="!isNew"
|
|
||||||
class="btn btn-danger"
|
|
||||||
type="button"
|
|
||||||
:disabled="saving"
|
|
||||||
@click="remove"
|
|
||||||
>
|
|
||||||
حذف
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<section v-if="showCsat" class="csat">
|
<UiCard v-if="showCsat" class="csat-card">
|
||||||
<h2>رضایت مشتری (CSAT)</h2>
|
<template #header>
|
||||||
<div class="csat-scores">
|
<div class="csat-header">
|
||||||
<button
|
<h3>رضایت مشتری (CSAT)</h3>
|
||||||
v-for="n in 5"
|
<span v-if="ticket?.csatAt" class="muted">
|
||||||
:key="n"
|
آخرین ثبت: {{ formatWhen(ticket.csatAt) }}
|
||||||
type="button"
|
</span>
|
||||||
class="score"
|
</div>
|
||||||
:class="{ active: csatScore === n }"
|
</template>
|
||||||
@click="csatScore = n"
|
<div class="csat-body">
|
||||||
>
|
<div class="csat-scores">
|
||||||
{{ n }}
|
<button
|
||||||
</button>
|
v-for="n in 5"
|
||||||
|
:key="n"
|
||||||
|
type="button"
|
||||||
|
class="score"
|
||||||
|
:class="{ active: csatScore === n }"
|
||||||
|
@click="csatScore = n"
|
||||||
|
>
|
||||||
|
{{ n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<UiTextarea
|
||||||
|
v-model="csatComment"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="نظر یا بازخورد مشتری (اختیاری)…"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<template #footer>
|
||||||
v-model="csatComment"
|
<UiButton
|
||||||
rows="2"
|
variant="secondary"
|
||||||
placeholder="نظر اختیاری…"
|
:disabled="csatSaving || csatScore == null"
|
||||||
/>
|
:loading="csatSaving"
|
||||||
<button
|
@click="saveCsat"
|
||||||
class="btn"
|
>
|
||||||
type="button"
|
{{ ticket?.csatAt ? 'بهروزرسانی رضایت' : 'ثبت رضایت' }}
|
||||||
:disabled="csatSaving || csatScore == null"
|
</UiButton>
|
||||||
@click="saveCsat"
|
</template>
|
||||||
>
|
</UiCard>
|
||||||
{{ csatSaving ? '…' : ticket?.csatAt ? 'بهروزرسانی رضایت' : 'ثبت رضایت' }}
|
|
||||||
</button>
|
<UiCard v-if="!isNew && ticket" class="thread-card">
|
||||||
<p v-if="ticket?.csatAt" class="muted">
|
<template #header>
|
||||||
آخرین ثبت: {{ formatWhen(ticket.csatAt) }}
|
<div class="thread-header">
|
||||||
</p>
|
<h3>گفتگو و پیامها</h3>
|
||||||
</section>
|
<UiBadge variant="neutral" size="sm">
|
||||||
|
{{ ticket.messages.length }} پیام
|
||||||
|
</UiBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<section v-if="!isNew && ticket" class="thread">
|
|
||||||
<h2>گفتگو</h2>
|
|
||||||
<div v-if="ticket.messages.length" class="messages">
|
<div v-if="ticket.messages.length" class="messages">
|
||||||
<article
|
<article
|
||||||
v-for="msg in ticket.messages"
|
v-for="msg in ticket.messages"
|
||||||
@@ -431,261 +497,233 @@ onMounted(load);
|
|||||||
<header>
|
<header>
|
||||||
<strong>{{ msg.author?.fullName || 'سیستم' }}</strong>
|
<strong>{{ msg.author?.fullName || 'سیستم' }}</strong>
|
||||||
<span class="muted">{{ formatWhen(msg.createdAt) }}</span>
|
<span class="muted">{{ formatWhen(msg.createdAt) }}</span>
|
||||||
<span v-if="msg.isInternal" class="tag">داخلی</span>
|
<UiBadge v-if="msg.isInternal" variant="warning" size="sm">داخلی</UiBadge>
|
||||||
</header>
|
</header>
|
||||||
<p>{{ msg.body }}</p>
|
<p>{{ msg.body }}</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="muted">هنوز پیامی نیست.</p>
|
<p v-else class="muted empty-messages">هنوز پیامی ثبت نشده است.</p>
|
||||||
|
|
||||||
<form class="reply" @submit.prevent="sendReply">
|
<template #footer>
|
||||||
<textarea
|
<form class="reply-form" @submit.prevent="sendReply">
|
||||||
v-model="reply"
|
<UiTextarea
|
||||||
rows="3"
|
v-model="reply"
|
||||||
placeholder="پاسخ یا یادداشت…"
|
:rows="3"
|
||||||
required
|
placeholder="پاسخ به تیکت یا یادداشت جدید…"
|
||||||
/>
|
required
|
||||||
<label class="check">
|
/>
|
||||||
<input v-model="replyInternal" type="checkbox" />
|
<div class="reply-actions">
|
||||||
یادداشت داخلی (فقط تیم)
|
<label class="check">
|
||||||
</label>
|
<input v-model="replyInternal" type="checkbox" />
|
||||||
<button class="btn" type="submit" :disabled="messaging">
|
<span>یادداشت داخلی (فقط همکاران)</span>
|
||||||
{{ messaging ? '…' : 'ارسال' }}
|
</label>
|
||||||
</button>
|
<UiButton
|
||||||
</form>
|
variant="primary"
|
||||||
</section>
|
type="submit"
|
||||||
|
:disabled="messaging || !reply.trim()"
|
||||||
|
:loading="messaging"
|
||||||
|
>
|
||||||
|
ارسال پیام
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</UiCard>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
max-width: 760px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.head-copy {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crumb {
|
.crumb {
|
||||||
margin: 0 0 0.25rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
gap: var(--sp-2);
|
||||||
color: #94a3b8;
|
color: var(--color-text-muted);
|
||||||
font-size: 0.82rem;
|
font-size: var(--fs-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.crumb a {
|
.crumb a {
|
||||||
color: #0f766e;
|
color: var(--color-primary-dark);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: clamp(1.25rem, 2.5vw, 1.55rem);
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0 0 0.75rem;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form,
|
|
||||||
.thread,
|
|
||||||
.csat {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form,
|
|
||||||
.thread,
|
|
||||||
.csat {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 1rem;
|
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-box {
|
.sla-box {
|
||||||
padding: 0.75rem 1rem;
|
display: flex;
|
||||||
border-radius: 18px;
|
align-items: center;
|
||||||
border: 1px solid #99f6e4;
|
gap: var(--sp-3);
|
||||||
background: #f0fdfa;
|
padding: var(--sp-3) var(--sp-4);
|
||||||
font-size: 0.95rem;
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--color-primary-light);
|
||||||
|
background: var(--color-primary-surface);
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sla-box.bad {
|
.sla-box.bad {
|
||||||
border-color: color-mix(in srgb, #b42318 40%, var(--line));
|
border-color: color-mix(in srgb, var(--color-danger) 40%, var(--color-border));
|
||||||
background: color-mix(in srgb, #b42318 8%, #fff);
|
background: var(--color-danger-surface);
|
||||||
color: #b42318;
|
color: var(--color-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.faq-hints-card {
|
||||||
display: grid;
|
border: 1px dashed var(--color-border);
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
background: var(--color-surface-sunken);
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field.wide {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select,
|
|
||||||
textarea {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
min-height: var(--control-h);
|
|
||||||
padding: 0 0.85rem;
|
|
||||||
font: inherit;
|
|
||||||
background: #fff;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
min-height: 5.5rem;
|
|
||||||
padding: 0.65rem 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-hints {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.45rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: 1px dashed var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.faq-title {
|
.faq-title {
|
||||||
margin: 0;
|
margin: 0 0 var(--sp-2);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.9rem;
|
font-size: var(--fs-sm);
|
||||||
}
|
|
||||||
|
|
||||||
.faq-hints details {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.faq-hints p {
|
|
||||||
margin: 0.4rem 0 0;
|
|
||||||
color: var(--muted);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.6rem;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
gap: var(--sp-2);
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.csat,
|
.faq-item {
|
||||||
.thread {
|
font-size: var(--fs-sm);
|
||||||
background: var(--surface);
|
margin-top: var(--sp-2);
|
||||||
border: 1px solid var(--line);
|
}
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 1rem;
|
.faq-item summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-emphasis);
|
||||||
|
}
|
||||||
|
|
||||||
|
.faq-item p {
|
||||||
|
margin: var(--sp-2) 0 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
color: var(--color-danger);
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.csat-card,
|
||||||
|
.thread-card {
|
||||||
|
margin-top: var(--sp-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.csat-header,
|
||||||
|
.thread-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.csat-header h3,
|
||||||
|
.thread-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--fs-base);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.csat-body {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.csat-scores {
|
.csat-scores {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.4rem;
|
gap: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.score {
|
.score {
|
||||||
width: 2.4rem;
|
width: 2.6rem;
|
||||||
height: 2.4rem;
|
height: 2.6rem;
|
||||||
border-radius: 10px;
|
border-radius: var(--radius-md);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--color-border);
|
||||||
background: #fff;
|
background: var(--color-surface);
|
||||||
font: inherit;
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--fs-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: var(--color-primary-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.score.active {
|
.score.active {
|
||||||
border-color: var(--accent);
|
border-color: var(--color-primary);
|
||||||
background: color-mix(in srgb, var(--accent) 12%, #fff);
|
background: var(--color-primary);
|
||||||
font-weight: 700;
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.65rem;
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg {
|
.msg {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius-lg);
|
||||||
padding: 0.75rem 0.9rem;
|
padding: var(--sp-3) var(--sp-4);
|
||||||
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg.internal {
|
.msg.internal {
|
||||||
background: color-mix(in srgb, var(--accent) 6%, #fff);
|
background: var(--color-warning-surface);
|
||||||
|
border-color: color-mix(in srgb, var(--color-warning) 30%, var(--color-border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg header {
|
.msg header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: var(--sp-2);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0.35rem;
|
margin-bottom: var(--sp-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg p {
|
.msg p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.empty-messages {
|
||||||
font-size: 0.75rem;
|
font-size: var(--fs-sm);
|
||||||
color: var(--accent);
|
padding: var(--sp-4) 0;
|
||||||
font-weight: 700;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply {
|
.reply-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.55rem;
|
gap: var(--sp-3);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.check {
|
.check {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.4rem;
|
gap: var(--sp-2);
|
||||||
color: inherit;
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
color: var(--muted);
|
color: var(--color-text-muted);
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: #b42318;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import { RouterLink, useRoute, useRouter } from 'vue-router';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { toFaDigits } from '@/lib/jalali';
|
import { toFaDigits } from '@/lib/jalali';
|
||||||
|
import {
|
||||||
|
UiButton,
|
||||||
|
UiPageHeader,
|
||||||
|
UiEmptyState,
|
||||||
|
} from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
import type {
|
import type {
|
||||||
PaginatedResponse,
|
PaginatedResponse,
|
||||||
@@ -127,15 +132,19 @@ onMounted(load);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page" @click="filterOpen = false">
|
<section class="page" @click="filterOpen = false">
|
||||||
<header class="page-head">
|
<UiPageHeader
|
||||||
<div class="head-copy">
|
title="تیکتها"
|
||||||
<h1>تیکتها</h1>
|
lead="درخواستهای پشتیبانی را ثبت و پیگیری کنید."
|
||||||
<p class="lead">درخواستهای پشتیبانی را ثبت و پیگیری کنید.</p>
|
>
|
||||||
</div>
|
<template #actions>
|
||||||
<RouterLink class="add-btn" :to="{ name: 'ticket-new' }" aria-label="تیکت جدید">
|
<RouterLink :to="{ name: 'ticket-new' }">
|
||||||
<span>+</span>
|
<UiButton variant="primary">
|
||||||
</RouterLink>
|
<UiIcon name="plus" :size="16" />
|
||||||
</header>
|
تیکت جدید
|
||||||
|
</UiButton>
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
|
</UiPageHeader>
|
||||||
|
|
||||||
<div class="tools">
|
<div class="tools">
|
||||||
<form class="search" @submit.prevent="onSearch">
|
<form class="search" @submit.prevent="onSearch">
|
||||||
@@ -177,25 +186,20 @@ onMounted(load);
|
|||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
||||||
|
|
||||||
<div v-else-if="listEmpty" class="empty-card">
|
<UiEmptyState
|
||||||
<div class="art" aria-hidden="true">
|
v-else-if="listEmpty"
|
||||||
<svg viewBox="0 0 168 148" fill="none">
|
title="هنوز تیکتی ثبت نشده است"
|
||||||
<rect x="38" y="18" width="92" height="112" rx="16" fill="#E7F6F3" />
|
description="اولین درخواست پشتیبانی را ثبت کنید تا پیگیریها اینجا جمع شوند."
|
||||||
<rect x="50" y="32" width="68" height="84" rx="10" fill="#fff" />
|
>
|
||||||
<path d="M62 52h28" stroke="#14b8a6" stroke-width="4" stroke-linecap="round" />
|
<template #action>
|
||||||
<path d="M62 68h36" stroke="#99f6e4" stroke-width="4" stroke-linecap="round" />
|
<RouterLink :to="{ name: 'ticket-new' }">
|
||||||
<path d="M62 84h22" stroke="#99f6e4" stroke-width="4" stroke-linecap="round" />
|
<UiButton variant="primary">
|
||||||
<circle cx="118" cy="112" r="20" fill="#14b8a6" />
|
<UiIcon name="plus" :size="16" />
|
||||||
<path d="M118 104v16M110 112h16" stroke="#fff" stroke-width="3" stroke-linecap="round" />
|
تیکت جدید
|
||||||
</svg>
|
</UiButton>
|
||||||
</div>
|
</RouterLink>
|
||||||
<h2>هنوز تیکتی نیست</h2>
|
</template>
|
||||||
<p>اولین درخواست پشتیبانی را ثبت کنید تا پیگیریها اینجا جمع شوند.</p>
|
</UiEmptyState>
|
||||||
<RouterLink class="btn primary wide" :to="{ name: 'ticket-new' }">
|
|
||||||
<span>+</span>
|
|
||||||
تیکت جدید
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="list-head">
|
<div class="list-head">
|
||||||
@@ -304,7 +308,7 @@ onMounted(load);
|
|||||||
h1 {
|
h1 {
|
||||||
margin: 0 0 0.3rem;
|
margin: 0 0 0.3rem;
|
||||||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lead,
|
.lead,
|
||||||
@@ -346,25 +350,38 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
flex: 1;
|
flex: 1 1 200px;
|
||||||
min-width: 0;
|
min-width: 180px;
|
||||||
|
max-width: 400px;
|
||||||
|
height: var(--control-h);
|
||||||
|
min-height: var(--control-h);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
padding: 0 0.85rem;
|
padding: 0 0.75rem;
|
||||||
color: #94a3b8;
|
color: var(--muted);
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search:focus-within {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search input {
|
.search input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
outline: none;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0.75rem 0;
|
padding: 0;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-wrap {
|
.filter-wrap {
|
||||||
@@ -377,31 +394,40 @@ h1 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
|
height: var(--control-h);
|
||||||
min-height: var(--control-h);
|
min-height: var(--control-h);
|
||||||
padding: 0 0.95rem;
|
padding: 0 0.85rem;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
|
font-size: var(--fs-sm);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.btn.primary {
|
||||||
background: #14b8a6;
|
background: var(--accent);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.ghost {
|
.btn.ghost {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #0f766e;
|
color: var(--ink-secondary);
|
||||||
border-color: #99f6e4;
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.ghost:hover {
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
border-color: var(--line-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.ghost.on {
|
.btn.ghost.on {
|
||||||
background: #f0fdfa;
|
background: var(--accent-subtle);
|
||||||
border-color: #14b8a6;
|
border-color: var(--accent);
|
||||||
|
color: var(--accent-deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.wide {
|
.btn.wide {
|
||||||
@@ -427,7 +453,7 @@ h1 {
|
|||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
min-width: 11rem;
|
min-width: 11rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.14);
|
||||||
padding: 0.35rem;
|
padding: 0.35rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -441,7 +467,7 @@ h1 {
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0.6rem 0.7rem;
|
padding: 0.6rem 0.7rem;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
@@ -455,7 +481,7 @@ h1 {
|
|||||||
|
|
||||||
.empty-card {
|
.empty-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 22px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.05);
|
||||||
padding: 2.4rem 1.2rem 2rem;
|
padding: 2.4rem 1.2rem 2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -487,7 +513,7 @@ h1 {
|
|||||||
|
|
||||||
.table-wrap {
|
.table-wrap {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 18px;
|
border-radius: var(--radius);
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
@@ -601,7 +627,7 @@ h1 {
|
|||||||
|
|
||||||
.m-card {
|
.m-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: var(--radius);
|
||||||
padding: 0.95rem 1rem;
|
padding: 0.95rem 1rem;
|
||||||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Preview — copy to /var/www/crm2/apps/api/.env
|
||||||
|
# Start from production crm .env, then change only the keys below.
|
||||||
|
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3032
|
||||||
|
CORS_ORIGIN=https://crm2.montzar.ir
|
||||||
|
JWT_SECRET=change-me-long-random-production-secret
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
# Separate database — do not point this at mento_crm
|
||||||
|
DATABASE_URL="postgresql://mento:CHANGE_ME@127.0.0.1:15432/mento_crm2?schema=public"
|
||||||
|
|
||||||
|
UPLOAD_DIR=/var/www/crm2/uploads
|
||||||
|
|
||||||
|
PORTAL_SHOW_OTP=false
|
||||||
|
AUTH_RETURN_OTP=false
|
||||||
|
AUTH_EMAIL_OTP_ENABLED=false
|
||||||
|
|
||||||
|
# Copy SMS / AI / STT / encryption keys from /var/www/crm/apps/api/.env
|
||||||
|
SMS_PROVIDER=trez
|
||||||
|
SMS_USERNAME=
|
||||||
|
SMS_PASSWORD=
|
||||||
|
SMS_SENDER=
|
||||||
|
SMS_BASE_URL=http://smspanel.Trez.ir
|
||||||
|
SMS_SEND_PATH=/SendMessageWithCode.ashx
|
||||||
|
SMS_OTP_TEMPLATE=کد ورود پرتال منتکت: {code}
|
||||||
|
|
||||||
|
AI_ENABLED=true
|
||||||
|
AI_PROVIDER=openai
|
||||||
|
AI_BASE_URL=https://api.gapgpt.app/v1
|
||||||
|
AI_API_KEY=change-me
|
||||||
|
AI_MODEL=gpt-4o-mini
|
||||||
|
AI_TIMEOUT_MS=45000
|
||||||
|
|
||||||
|
# Share the existing STT sidecar (do not start a second one)
|
||||||
|
STT_ENABLED=true
|
||||||
|
STT_URL=http://127.0.0.1:3040
|
||||||
|
STT_INTERNAL_TOKEN=change-me-stt-internal-token
|
||||||
|
STT_TIMEOUT_MS=60000
|
||||||
|
|
||||||
|
INTEGRATIONS_ENCRYPTION_KEY=change-me-64-hex-chars-use-openssl-rand-hex-32
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
# پیشنمایش دیزاین — `crm2.montzar.ir`
|
||||||
|
|
||||||
|
نسخهٔ دوم کنار پروداکشن. **به `/var/www/crm`، `mento-crm-api`، پورت `3030` و دیتابیس `mento_crm` دست نزنید.**
|
||||||
|
|
||||||
|
| | پروداکشن | پیشنمایش |
|
||||||
|
|---|---|---|
|
||||||
|
| دامنه | `crm.montzar.ir` | `crm2.montzar.ir` |
|
||||||
|
| مسیر | `/var/www/crm` | `/var/www/crm2` |
|
||||||
|
| برنچ | `main` | `design_ui` |
|
||||||
|
| پورت API | `3030` | `3032` |
|
||||||
|
| PM2 | `mento-crm-api` | `mento-crm2-api` |
|
||||||
|
| دیتابیس | `mento_crm` | `mento_crm2` |
|
||||||
|
| STT | `mento-stt` روی `3040` | همان را share کنید |
|
||||||
|
|
||||||
|
بروزرسانی پروداکشن همچنان فقط بلوک قفلشده در `deploy/DEPLOY.md` است.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## قبل از سرور
|
||||||
|
|
||||||
|
1. DNS: رکورد `A` برای `crm2.montzar.ir` همان IP پروداکشن (`185.50.38.53`) — مثل `crm.montzar.ir`.
|
||||||
|
2. اول `design_ui` را push کنید، بعد کلون/پول روی سرور.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## بار اول روی سرور
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -p 3031 montime@185.50.38.53
|
||||||
|
|
||||||
|
sudo mkdir -p /var/www/crm2
|
||||||
|
sudo chown montime:montime /var/www/crm2
|
||||||
|
git clone --branch design_ui http://get.feep.ir/Mahdi/CRM_Montzar.git /var/www/crm2
|
||||||
|
|
||||||
|
# دیتابیس جدا (خالی امنتر است؛ برای دادهٔ شبیه پروداکشن dump بگیرید)
|
||||||
|
# CREATE DATABASE mento_crm2 OWNER mento;
|
||||||
|
# اختیاری:
|
||||||
|
# pg_dump -p 15432 -d mento_crm | psql -p 15432 -d mento_crm2
|
||||||
|
|
||||||
|
cp /var/www/crm/apps/api/.env /var/www/crm2/apps/api/.env
|
||||||
|
nano /var/www/crm2/apps/api/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
در `.env` فقط اینها را عوض کنید:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PORT=3032
|
||||||
|
CORS_ORIGIN=https://crm2.montzar.ir
|
||||||
|
DATABASE_URL="postgresql://USER:PASS@127.0.0.1:15432/mento_crm2?schema=public"
|
||||||
|
UPLOAD_DIR=/var/www/crm2/uploads
|
||||||
|
```
|
||||||
|
|
||||||
|
`STT_URL` را روی `http://127.0.0.1:3040` بگذارید. `mento-stt` را دوباره start نکنید.
|
||||||
|
|
||||||
|
وابستگیها (کپی از پروداکشن تا `npm ci` لازم نباشد):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -a /var/www/crm/node_modules /var/www/crm2/
|
||||||
|
rm -rf /var/www/crm2/node_modules/@mento/shared
|
||||||
|
ln -s ../../packages/shared /var/www/crm2/node_modules/@mento/shared
|
||||||
|
chmod +x /var/www/crm2/node_modules/.bin/*
|
||||||
|
find /var/www/crm2/node_modules -path '*esbuild*' -name 'esbuild' -type f -exec chmod +x {} \;
|
||||||
|
```
|
||||||
|
|
||||||
|
بیلد و migrate **فقط روی crm2**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /var/www/crm2
|
||||||
|
npm run build -w @mento/shared
|
||||||
|
cd apps/api && npx prisma migrate deploy && npx prisma generate && cd ../..
|
||||||
|
npm run build -w @mento/api
|
||||||
|
npm run build -w @mento/web
|
||||||
|
mkdir -p uploads
|
||||||
|
|
||||||
|
pm2 start /var/www/crm2/deploy/ecosystem-crm2.config.cjs
|
||||||
|
pm2 save
|
||||||
|
curl -sI http://127.0.0.1:3032/api/health
|
||||||
|
|
||||||
|
sudo cp /var/www/crm2/deploy/nginx-crm2.montzar.ir.conf /etc/nginx/sites-available/crm2.montzar.ir
|
||||||
|
sudo ln -sf /etc/nginx/sites-available/crm2.montzar.ir /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
vhost را HTTP :80 نگه دارید (پروکسی جلویی SSL را تمام میکند). Certbot روی این باکس نزنید.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## آپدیت بعدی پیشنمایش (بعد از push روی `design_ui`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -p 3031 montime@185.50.38.53
|
||||||
|
cd /var/www/crm2
|
||||||
|
git fetch origin
|
||||||
|
git checkout design_ui
|
||||||
|
git pull origin design_ui
|
||||||
|
|
||||||
|
npm run build -w @mento/shared
|
||||||
|
cd apps/api && npx prisma migrate deploy && npx prisma generate && cd ../..
|
||||||
|
npm run build -w @mento/api
|
||||||
|
npm run build -w @mento/web
|
||||||
|
pm2 restart mento-crm2-api
|
||||||
|
```
|
||||||
|
|
||||||
|
`pm2 restart mento-crm-api` نکنید. `cd /var/www/crm` نکنید.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* PM2 — Mento CRM preview (crm2.montzar.ir)
|
||||||
|
* Port 3032. Does not start STT; reuse mento-stt on 3040.
|
||||||
|
*
|
||||||
|
* pm2 start /var/www/crm2/deploy/ecosystem-crm2.config.cjs
|
||||||
|
* pm2 save
|
||||||
|
*
|
||||||
|
* Never restart mento-crm-api from this file.
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "mento-crm2-api",
|
||||||
|
cwd: "/var/www/crm2/apps/api",
|
||||||
|
script: "dist/src/main.js",
|
||||||
|
instances: 1,
|
||||||
|
exec_mode: "fork",
|
||||||
|
env: {
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: "3032",
|
||||||
|
},
|
||||||
|
max_memory_restart: "600M",
|
||||||
|
time: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Preview instance — crm2.montzar.ir
|
||||||
|
# Place under /etc/nginx/sites-available/crm2.montzar.ir
|
||||||
|
# HTTP only (same as crm): front proxy terminates SSL.
|
||||||
|
#
|
||||||
|
# Do not replace crm.montzar.ir / adm / ktg / montime vhosts.
|
||||||
|
|
||||||
|
upstream mento_crm2_api {
|
||||||
|
server 127.0.0.1:3032;
|
||||||
|
keepalive 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name crm2.montzar.ir;
|
||||||
|
|
||||||
|
client_max_body_size 50M;
|
||||||
|
|
||||||
|
root /var/www/crm2/apps/web/dist;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://mento_crm2_api;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
-26
@@ -287,7 +287,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"aix"
|
"aix"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -305,7 +304,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -323,7 +321,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -341,7 +338,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -359,7 +355,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -377,7 +372,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -395,7 +389,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -413,7 +406,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -431,7 +423,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -449,7 +440,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -467,7 +457,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -485,7 +474,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -503,7 +491,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -521,7 +508,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -539,7 +525,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -557,7 +542,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -575,7 +559,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -593,7 +576,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"netbsd"
|
"netbsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -611,7 +593,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"netbsd"
|
"netbsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -629,7 +610,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"openbsd"
|
"openbsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -647,7 +627,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"openbsd"
|
"openbsd"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -665,7 +644,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"openharmony"
|
"openharmony"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -683,7 +661,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"sunos"
|
"sunos"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -701,7 +678,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -719,7 +695,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -737,7 +712,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user