Align desktop UI with mobile shop flows and polish navigation
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
/* Brand — sampled from mockups/11-my-profile + FAB/CTA screens */
|
||||
--color-primary: #0a353c;
|
||||
--color-primary-hover: #072a2f;
|
||||
--color-primary-soft: #e8f4f1;
|
||||
--color-primary-soft: color-mix(in srgb, var(--color-primary) 14%, #ffffff);
|
||||
--color-primary-dark: #062428;
|
||||
--color-primary-light: #e8f4f1;
|
||||
--color-primary-light: var(--color-primary-soft);
|
||||
--color-primary-rgb: 10, 53, 60;
|
||||
--color-accent-navy: #0a353c;
|
||||
--color-accent-navy-rgb: 10, 53, 60;
|
||||
@@ -65,9 +65,9 @@
|
||||
html.dark {
|
||||
--color-primary: #5eb8ad;
|
||||
--color-primary-hover: #7ad0c5;
|
||||
--color-primary-soft: #14353a;
|
||||
--color-primary-soft: color-mix(in srgb, var(--color-primary) 22%, var(--color-bg));
|
||||
--color-primary-dark: #7ad0c5;
|
||||
--color-primary-light: #14353a;
|
||||
--color-primary-light: var(--color-primary-soft);
|
||||
--color-primary-rgb: 94, 184, 173;
|
||||
--color-accent-navy: #5eb8ad;
|
||||
--color-accent-navy-rgb: 94, 184, 173;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useCrmStore } from '@/stores/crm'
|
||||
import BaseInput from '@/components/ui/BaseInput.vue'
|
||||
@@ -15,6 +15,18 @@ import {
|
||||
} from '@/utils/formErrors'
|
||||
import { ApiError } from '@/services/http'
|
||||
|
||||
export type CustomerFormInitial = {
|
||||
firstName: string
|
||||
lastName: string
|
||||
phone: string
|
||||
businessName?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
customerId?: string
|
||||
initial?: CustomerFormInitial | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [id?: string]
|
||||
}>()
|
||||
@@ -30,6 +42,23 @@ const fieldErrors = ref<FieldErrors>({})
|
||||
const formError = ref('')
|
||||
const saving = ref(false)
|
||||
|
||||
const isEdit = () => Boolean(props.customerId)
|
||||
|
||||
function applyInitial(value?: CustomerFormInitial | null) {
|
||||
firstName.value = value?.firstName ?? ''
|
||||
lastName.value = value?.lastName ?? ''
|
||||
phone.value = value?.phone ?? ''
|
||||
businessName.value = value?.businessName ?? ''
|
||||
fieldErrors.value = {}
|
||||
formError.value = ''
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.initial,
|
||||
(value) => applyInitial(value),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function clearField(key: string) {
|
||||
fieldErrors.value = clearFieldError(fieldErrors.value, key)
|
||||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||||
@@ -56,19 +85,30 @@ async function submit() {
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
firstName: firstName.value.trim(),
|
||||
lastName: lastName.value.trim(),
|
||||
phone: normalized,
|
||||
businessName: businessName.value.trim() || undefined,
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const created = (await crm.registerCustomer({
|
||||
firstName: firstName.value.trim(),
|
||||
lastName: lastName.value.trim(),
|
||||
phone: normalized,
|
||||
businessName: businessName.value.trim() || undefined,
|
||||
})) as { id?: string }
|
||||
if (isEdit() && props.customerId) {
|
||||
const updated = (await crm.updateCustomer(props.customerId, payload)) as { id?: string }
|
||||
emit('success', updated?.id ?? props.customerId)
|
||||
return
|
||||
}
|
||||
|
||||
const created = (await crm.registerCustomer(payload)) as { id?: string }
|
||||
emit('success', created?.id)
|
||||
} catch (e) {
|
||||
formError.value =
|
||||
e instanceof ApiError || e instanceof Error ? e.message : 'ثبت مشتری ناموفق بود.'
|
||||
e instanceof ApiError || e instanceof Error
|
||||
? e.message
|
||||
: isEdit()
|
||||
? 'ویرایش مشتری ناموفق بود.'
|
||||
: 'ثبت مشتری ناموفق بود.'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -79,7 +119,7 @@ defineExpose({ submit, saving })
|
||||
|
||||
<template>
|
||||
<div class="customer-create-form">
|
||||
<p class="customer-create-form__intro">
|
||||
<p v-if="!isEdit()" class="customer-create-form__intro">
|
||||
مشتری به لیست شما اضافه میشود و بعد از ورود با همین شماره، سفارشهایش به حساب شما وصل میماند.
|
||||
</p>
|
||||
|
||||
@@ -118,7 +158,7 @@ defineExpose({ submit, saving })
|
||||
</div>
|
||||
|
||||
<BaseButton block :loading="saving" :disabled="saving" @click="submit">
|
||||
ثبت مشتری
|
||||
{{ isEdit() ? 'ذخیره تغییرات' : 'ثبت مشتری' }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, watch } from 'vue'
|
||||
import CustomerCreateForm from '@/components/customers/CustomerCreateForm.vue'
|
||||
import { computed, onBeforeUnmount, watch } from 'vue'
|
||||
import CustomerCreateForm, {
|
||||
type CustomerFormInitial,
|
||||
} from '@/components/customers/CustomerCreateForm.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
customerId?: string
|
||||
initial?: CustomerFormInitial | null
|
||||
title?: string
|
||||
}>()
|
||||
|
||||
const sheetTitle = computed(() => props.title ?? (props.customerId ? 'ویرایش مشتری' : 'افزودن مشتری'))
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
success: [id?: string]
|
||||
@@ -56,7 +63,7 @@ onBeforeUnmount(() => {
|
||||
<div class="customer-create-sheet">
|
||||
<div class="customer-create-sheet__handle" aria-hidden="true" />
|
||||
<header class="customer-create-sheet__head">
|
||||
<h2 id="customer-create-title">افزودن مشتری</h2>
|
||||
<h2 id="customer-create-title">{{ sheetTitle }}</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="customer-create-sheet__close"
|
||||
@@ -67,7 +74,11 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<CustomerCreateForm @success="onSuccess" />
|
||||
<CustomerCreateForm
|
||||
:customer-id="customerId"
|
||||
:initial="initial"
|
||||
@success="onSuccess"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
@@ -14,7 +14,7 @@ function navigate(path: string, event: Event) {
|
||||
const target = event.currentTarget
|
||||
if (target instanceof HTMLElement) target.blur()
|
||||
|
||||
// «ثبت سفارش»: سبد خالی → کالیتهها؛ در غیر این صورت سبد خرید
|
||||
// «ثبت سفارش»: سبد خالی → دستهبندی؛ در غیر این صورت سبد خرید
|
||||
if (path === '/cart') {
|
||||
router.push(resolveCartNavPath(cartStore.itemCount))
|
||||
return
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { usePublicContact } from '@/composables/usePublicContact'
|
||||
import {
|
||||
DESKTOP_NAV_ITEMS,
|
||||
isNavItemActive,
|
||||
@@ -14,6 +15,11 @@ import IconQemashMark from '@/components/icons/IconQemashMark.vue'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const { load, supportPhone, supportHref } = usePublicContact()
|
||||
|
||||
onMounted(() => {
|
||||
void load()
|
||||
})
|
||||
|
||||
const cartCount = computed(() => cartStore.itemCount)
|
||||
const activeCompanyId = computed(
|
||||
@@ -66,7 +72,16 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
<p class="desktop-sidebar__support-text">
|
||||
در صورت نیاز به راهنمایی یا گزارش مشکل با تیم پشتیبانی تماس بگیرید.
|
||||
</p>
|
||||
<a href="tel:02112345678" class="desktop-sidebar__support-btn">تماس با پشتیبانی</a>
|
||||
<a
|
||||
:href="supportHref ?? '#'"
|
||||
class="desktop-sidebar__support-btn"
|
||||
:class="{ 'desktop-sidebar__support-btn--disabled': !supportHref }"
|
||||
:aria-disabled="!supportHref"
|
||||
@click="!supportHref && $event.preventDefault()"
|
||||
>
|
||||
تماس با پشتیبانی
|
||||
</a>
|
||||
<p v-if="supportPhone" class="desktop-sidebar__support-phone" dir="ltr">{{ supportPhone }}</p>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -273,4 +288,19 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
.desktop-sidebar__support-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.desktop-sidebar__support-btn--disabled {
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.desktop-sidebar__support-phone {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
@@ -16,7 +20,7 @@ defineEmits<{
|
||||
|
||||
<template>
|
||||
<template v-if="centered">
|
||||
<header class="page-nav">
|
||||
<header class="page-nav" v-bind="$attrs">
|
||||
<button
|
||||
v-if="showBack"
|
||||
type="button"
|
||||
@@ -36,7 +40,7 @@ defineEmits<{
|
||||
<p v-if="subtitle" class="page-nav__subtitle">{{ subtitle }}</p>
|
||||
</template>
|
||||
|
||||
<header v-else class="page-header">
|
||||
<header v-else class="page-header" v-bind="$attrs">
|
||||
<button
|
||||
v-if="showBack"
|
||||
type="button"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
@@ -7,13 +9,23 @@ defineProps<{
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
function focus() {
|
||||
inputRef.value?.focus()
|
||||
}
|
||||
|
||||
defineExpose({ focus })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-field">
|
||||
<input
|
||||
ref="inputRef"
|
||||
class="search-field__input"
|
||||
type="search"
|
||||
enterkeyhint="search"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { getPublicContact, telHref } from '@/services/settings'
|
||||
|
||||
type ContactState = {
|
||||
supportPhone: string | null
|
||||
supportEmail: string | null
|
||||
}
|
||||
|
||||
const state = ref<ContactState | null>(null)
|
||||
let loadPromise: Promise<void> | null = null
|
||||
|
||||
function ensureLoaded() {
|
||||
if (state.value) return Promise.resolve()
|
||||
if (!loadPromise) {
|
||||
loadPromise = getPublicContact()
|
||||
.then((data) => {
|
||||
state.value = data
|
||||
})
|
||||
.catch(() => {
|
||||
state.value = { supportPhone: null, supportEmail: null }
|
||||
})
|
||||
}
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
export function usePublicContact() {
|
||||
const supportPhone = computed(() => state.value?.supportPhone ?? null)
|
||||
const supportEmail = computed(() => state.value?.supportEmail ?? null)
|
||||
const supportHref = computed(() => telHref(supportPhone.value))
|
||||
|
||||
function load() {
|
||||
return ensureLoaded()
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
supportPhone,
|
||||
supportEmail,
|
||||
supportHref,
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -36,9 +36,7 @@ const router = createRouter({
|
||||
},
|
||||
{
|
||||
path: '/companies',
|
||||
name: 'companies',
|
||||
component: () => import('@/views/companies/CompanySelectView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
redirect: '/categories',
|
||||
},
|
||||
{
|
||||
path: '/categories',
|
||||
|
||||
@@ -69,16 +69,22 @@ export async function fetchCatalogCategories() {
|
||||
title: string
|
||||
subtitle: string
|
||||
imageUrl?: string
|
||||
icon?: string
|
||||
showTitle?: boolean
|
||||
showSubtitle?: boolean
|
||||
showIcon?: boolean
|
||||
}>
|
||||
seasons: Array<{
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
imageUrl?: string
|
||||
icon?: string
|
||||
ctaLabel?: string
|
||||
showTitle?: boolean
|
||||
showDescription?: boolean
|
||||
showCta?: boolean
|
||||
showIcon?: boolean
|
||||
}>
|
||||
}>('/catalog/categories')
|
||||
} catch {
|
||||
@@ -89,16 +95,22 @@ export async function fetchCatalogCategories() {
|
||||
title: string
|
||||
subtitle: string
|
||||
imageUrl?: string
|
||||
icon?: string
|
||||
showTitle?: boolean
|
||||
showSubtitle?: boolean
|
||||
showIcon?: boolean
|
||||
}>
|
||||
seasons: Array<{
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
imageUrl?: string
|
||||
icon?: string
|
||||
ctaLabel?: string
|
||||
showTitle?: boolean
|
||||
showDescription?: boolean
|
||||
showCta?: boolean
|
||||
showIcon?: boolean
|
||||
}>
|
||||
}>('/products/meta/categories')
|
||||
} catch {
|
||||
@@ -160,6 +172,10 @@ export async function createAgentCustomer(body: CreateAgentCustomerPayload) {
|
||||
return agentRequest('/agent/customers', { method: 'POST', body })
|
||||
}
|
||||
|
||||
export async function updateAgentCustomer(id: string, body: CreateAgentCustomerPayload) {
|
||||
return agentRequest(`/agent/customers/${id}`, { method: 'PATCH', body })
|
||||
}
|
||||
|
||||
export async function fetchAgentCustomerDetail(id: string) {
|
||||
return agentRequest<{
|
||||
customer: unknown
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { AgentProfileDto } from '@/services/agentApi'
|
||||
import {
|
||||
completeReminder as apiCompleteReminder,
|
||||
createAgentCustomer,
|
||||
updateAgentCustomer,
|
||||
createMeetingReport,
|
||||
createReminder as apiCreateReminder,
|
||||
fetchAgentCustomerDetail,
|
||||
@@ -161,6 +162,13 @@ export const useCrmStore = defineStore('crm', () => {
|
||||
return createAgentCustomer(body)
|
||||
}
|
||||
|
||||
async function updateCustomer(
|
||||
id: string,
|
||||
body: Parameters<typeof createAgentCustomer>[0],
|
||||
) {
|
||||
return updateAgentCustomer(id, body)
|
||||
}
|
||||
|
||||
async function loadReminders(filter?: string) {
|
||||
reminders.value = (await fetchReminders(filter)) as CrmReminder[]
|
||||
return reminders.value
|
||||
@@ -203,6 +211,7 @@ export const useCrmStore = defineStore('crm', () => {
|
||||
loadCustomerDetail,
|
||||
submitMeeting,
|
||||
registerCustomer,
|
||||
updateCustomer,
|
||||
loadReminders,
|
||||
loadReminderDetail,
|
||||
markReminderComplete,
|
||||
|
||||
@@ -8,6 +8,9 @@ export interface CrmCustomer {
|
||||
id: string
|
||||
name: string
|
||||
contactName: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
businessName?: string
|
||||
phone: string
|
||||
status: CustomerStatus
|
||||
avatarLetter: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Catalog } from '@/types'
|
||||
import type { CatalogProductCard, SeasonKind } from '@/types/crm'
|
||||
import { normalizeDigits } from '@/utils/format'
|
||||
|
||||
export type CatalogGalleryFilter = 'all' | 'newest' | 'popular' | 'season'
|
||||
export type PatternKind = 'plain' | 'melange' | 'patterned'
|
||||
@@ -11,6 +12,7 @@ export interface CatalogGalleryCard {
|
||||
title: string
|
||||
latinName: string
|
||||
subtitle: string
|
||||
code?: string
|
||||
imageUrl?: string
|
||||
patternCount: number
|
||||
badge?: 'new' | 'popular'
|
||||
@@ -47,6 +49,14 @@ export function catalogDisplayTitle(name: string) {
|
||||
return trimmed.includes('کالیته') ? trimmed : `کالیته ${trimmed}`
|
||||
}
|
||||
|
||||
export function normalizeCatalogSearchText(value: string) {
|
||||
return normalizeDigits(value)
|
||||
.replace(/\u064A/g, '\u06CC')
|
||||
.replace(/\u0643/g, '\u06A9')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
export function catalogLatinName(catalog: Catalog) {
|
||||
const fromDescription = catalog.description?.match(/[A-Za-z][A-Za-z0-9 .&-]{1,28}/)?.[0]?.trim()
|
||||
if (fromDescription) return fromDescription
|
||||
@@ -146,6 +156,7 @@ export function buildCatalogGallery(
|
||||
title: catalogDisplayTitle(catalog.name),
|
||||
latinName: catalogLatinName(catalog),
|
||||
subtitle: catalog.description?.trim() || USAGE_SUBTITLE[usage] || 'پارچههای منتخب',
|
||||
code: catalog.code,
|
||||
imageUrl: catalog.imageUrl || items.find((item) => item.imageUrl)?.imageUrl,
|
||||
patternCount,
|
||||
badge: isNew ? 'new' : isPopular ? 'popular' : undefined,
|
||||
@@ -161,13 +172,13 @@ export function filterCatalogGallery(
|
||||
filter: CatalogGalleryFilter,
|
||||
query: string,
|
||||
) {
|
||||
const needle = query.trim().toLowerCase()
|
||||
const needle = normalizeCatalogSearchText(query)
|
||||
let list = cards.filter((card) => {
|
||||
if (!needle) return true
|
||||
return [card.title, card.name, card.latinName, card.subtitle]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(needle)
|
||||
const haystack = normalizeCatalogSearchText(
|
||||
[card.title, card.name, card.latinName, card.subtitle, card.code ?? ''].join(' '),
|
||||
)
|
||||
return haystack.includes(needle)
|
||||
})
|
||||
|
||||
if (filter === 'newest') {
|
||||
|
||||
+32
-11
@@ -5,6 +5,8 @@ import IconOrders from '@/components/icons/IconOrders.vue'
|
||||
import IconProfile from '@/components/icons/IconProfile.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
import IconPlus from '@/components/icons/IconPlus.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
import IconSettings from '@/components/icons/IconSettings.vue'
|
||||
|
||||
export interface NavItem {
|
||||
name: string
|
||||
@@ -32,6 +34,8 @@ export const DESKTOP_NAV_ITEMS: NavItem[] = [
|
||||
{ name: 'catalogs', path: '/catalogs', label: 'کالیتهها', icon: IconCatalog },
|
||||
{ name: 'cart', path: '/cart', label: 'ثبت سفارش', icon: IconCart },
|
||||
{ name: 'orders', path: '/orders', label: 'سفارشها', icon: IconOrders },
|
||||
{ name: 'checks', path: '/profile/checks', label: 'لیست چکها', icon: IconCheckBook },
|
||||
{ name: 'settings', path: '/profile/display', label: 'تنظیمات', icon: IconSettings },
|
||||
{ name: 'profile', path: '/profile', label: 'پروفایل', icon: IconProfile },
|
||||
]
|
||||
|
||||
@@ -45,26 +49,43 @@ const CATALOG_PATHS = [
|
||||
'/catalogs',
|
||||
'/catalog-request',
|
||||
'/catalog-requests',
|
||||
'/categories',
|
||||
'/products',
|
||||
'/product',
|
||||
'/favorites',
|
||||
]
|
||||
|
||||
/** Shop browse flow: empty cart → categories → products → cart */
|
||||
const ORDER_BROWSE_PATHS = ['/categories', '/products', '/product', '/favorites']
|
||||
|
||||
const SETTINGS_PATHS = ['/profile/display', '/profile/edit', '/profile/security']
|
||||
|
||||
const PROFILE_SUBPAGE_EXCLUSIONS = ['/profile/checks', ...SETTINGS_PATHS]
|
||||
|
||||
function matchesPathPrefix(currentPath: string, prefixes: string[]): boolean {
|
||||
return prefixes.some((p) => currentPath === p || currentPath.startsWith(`${p}/`))
|
||||
}
|
||||
|
||||
export function isNavItemActive(name: string, path: string, currentPath: string): boolean {
|
||||
if (name === 'home') {
|
||||
return currentPath === '/'
|
||||
}
|
||||
if (name === 'catalogs') {
|
||||
return CATALOG_PATHS.some(
|
||||
(p) => currentPath === p || currentPath.startsWith(`${p}/`),
|
||||
)
|
||||
return matchesPathPrefix(currentPath, CATALOG_PATHS)
|
||||
}
|
||||
if (name === 'order-fab' || name === 'cart') {
|
||||
return currentPath === '/cart' || currentPath.startsWith('/cart/')
|
||||
return (
|
||||
currentPath === '/cart' ||
|
||||
currentPath.startsWith('/cart/') ||
|
||||
matchesPathPrefix(currentPath, ORDER_BROWSE_PATHS)
|
||||
)
|
||||
}
|
||||
if (name === 'checks') {
|
||||
return matchesPathPrefix(currentPath, ['/profile/checks'])
|
||||
}
|
||||
if (name === 'settings') {
|
||||
return matchesPathPrefix(currentPath, SETTINGS_PATHS)
|
||||
}
|
||||
if (name === 'profile') {
|
||||
return currentPath === '/profile' || currentPath.startsWith('/profile/')
|
||||
if (currentPath === '/profile') return true
|
||||
if (!currentPath.startsWith('/profile/')) return false
|
||||
return !matchesPathPrefix(currentPath, PROFILE_SUBPAGE_EXCLUSIONS)
|
||||
}
|
||||
if (path === '/') return currentPath === '/'
|
||||
return currentPath.startsWith(path)
|
||||
@@ -77,8 +98,8 @@ export function resolveNavPath(
|
||||
return item.path
|
||||
}
|
||||
|
||||
/** Empty cart: mirror mobile FAB → browse catalogs */
|
||||
/** Empty cart: browse products like a shop (categories first) */
|
||||
export function resolveCartNavPath(cartItemCount: number): string {
|
||||
if (cartItemCount === 0) return '/catalogs'
|
||||
if (cartItemCount === 0) return SHOP_BROWSE_PATH
|
||||
return '/cart'
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ const desktopFilterTabs: { id: DesktopOrderFilterId; label: string }[] = [
|
||||
:icon-variant="iconVariant"
|
||||
@update:tab="tab = $event"
|
||||
@select="goToOrder"
|
||||
@back="router.push('/')"
|
||||
@back="router.push('/categories')"
|
||||
@set-page="setMobilePage"
|
||||
/>
|
||||
|
||||
@@ -88,7 +88,7 @@ const desktopFilterTabs: { id: DesktopOrderFilterId; label: string }[] = [
|
||||
@set-desktop-filter="setDesktopFilter"
|
||||
@select="goToOrder"
|
||||
@set-page="setPage"
|
||||
@new-order="router.push('/catalogs')"
|
||||
@new-order="router.push('/categories')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useCartPage } from '@/composables/useCartPage'
|
||||
import { SHOP_BROWSE_PATH } from '@/utils/navigation'
|
||||
import BaseButton from '@/components/ui/BaseButton.vue'
|
||||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||||
import MobileCartPage from '@/views/cart/components/MobileCartPage.vue'
|
||||
@@ -24,7 +25,7 @@ const {
|
||||
} = useCartPage()
|
||||
|
||||
function goToOrdering() {
|
||||
router.push('/catalogs')
|
||||
router.push(SHOP_BROWSE_PATH)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -68,7 +69,7 @@ function goToOrdering() {
|
||||
<div class="dialog card">
|
||||
<h3>سقف متراژ تکمیل شد</h3>
|
||||
<p class="text-sm">
|
||||
سقف متراژ حداقل یک شرکت در سبد پر شده است. میتوانید همین پیشسفارشها را ثبت کنید یا سبد را خالی کرده و فاکتور جدید بسازید.
|
||||
سقف متراژ مجاز این سفارش پر شده است. میتوانید همین پیشسفارش را ثبت کنید یا سبد را خالی کرده و سفارش جدید بسازید.
|
||||
</p>
|
||||
<div class="dialog-actions">
|
||||
<BaseButton variant="outline" :loading="submitting" @click="proceedAnyway">
|
||||
|
||||
@@ -69,8 +69,8 @@ const quickLinks = [
|
||||
|
||||
<header class="cart-desktop-page__head">
|
||||
<h1 class="cart-desktop-page__title">سبد خرید</h1>
|
||||
<p class="cart-desktop-page__subtitle">
|
||||
{{ companyName ? `شرکتها: ${companyName}` : 'سبد خرید مشترک' }}
|
||||
<p v-if="companyName" class="cart-desktop-page__subtitle">
|
||||
تأمینکننده: {{ companyName }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -91,13 +91,13 @@ const quickLinks = [
|
||||
</p>
|
||||
<div class="cart-empty-card__actions">
|
||||
<button type="button" class="cart-btn cart-btn--primary" @click="emit('selectCompany')">
|
||||
<IconLayoutGrid :size="18" />
|
||||
مشاهده کالیتهها
|
||||
</button>
|
||||
<button type="button" class="cart-btn cart-btn--outline" @click="router.push('/categories')">
|
||||
<IconDiamond :size="16" />
|
||||
دستهبندی محصولات
|
||||
</button>
|
||||
<button type="button" class="cart-btn cart-btn--outline" @click="router.push('/catalogs')">
|
||||
<IconLayoutGrid :size="18" />
|
||||
مشاهده کالیتهها
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +107,6 @@ const quickLinks = [
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">محصول</th>
|
||||
<th scope="col">شرکت</th>
|
||||
<th scope="col">کد</th>
|
||||
<th scope="col">متراژ (متر)</th>
|
||||
<th scope="col">حذف</th>
|
||||
@@ -118,7 +117,6 @@ const quickLinks = [
|
||||
<td>
|
||||
<strong class="cart-items-table__name">{{ item.productName }}</strong>
|
||||
</td>
|
||||
<td>{{ item.companyName || '—' }}</td>
|
||||
<td>{{ item.productCode }}</td>
|
||||
<td>{{ formatNumber(item.meterage) }}</td>
|
||||
<td>
|
||||
@@ -192,7 +190,7 @@ const quickLinks = [
|
||||
{{ submitting ? 'در حال ثبت…' : 'ثبت پیشسفارش' }}
|
||||
</button>
|
||||
<p class="cart-summary-card__note">
|
||||
اقلام همه شرکتها در یک سبد و یک فاکتور ثبت میشوند؛ پس از قیمتگذاری، یکبار تسویه میکنید.
|
||||
پس از ثبت سفارش، مدیریت قیمتگذاری میکند و فاکتور صادر میشود؛ سپس روش تسویه را انتخاب میکنید.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -204,12 +202,12 @@ const quickLinks = [
|
||||
راهنمای گام بعدی
|
||||
</h3>
|
||||
<p class="cart-guide-card__text">
|
||||
پس از انتخاب شرکت و افزودن محصولات، متراژ کل و سقف مجاز فاکتور را بررسی کنید.
|
||||
محصولات موردنظر را به سبد اضافه کنید، متراژ کل را بررسی کنید و «ثبت پیشسفارش» را بزنید.
|
||||
<template v-if="!isEmpty">
|
||||
سقف مجاز: {{ formatMeterage(maxMeterage) }} — باقیمانده:
|
||||
{{ formatMeterage(remainingCapacity) }}.
|
||||
</template>
|
||||
سپس برای تکمیل سفارش، گزینه «ادامه و انتخاب تسویه» را بزنید.
|
||||
پس از قیمتگذاری، از صفحه سفارش روش تسویه را انتخاب میکنید.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -44,8 +44,8 @@ const emit = defineEmits<{
|
||||
</header>
|
||||
|
||||
<div v-if="!isEmpty" class="cart-mobile-page__body">
|
||||
<p class="cart-mobile-page__company">
|
||||
{{ companyName ? `شرکتها: ${companyName}` : 'سبد خرید مشترک' }}
|
||||
<p v-if="companyName" class="cart-mobile-page__meta">
|
||||
تأمینکننده: {{ companyName }}
|
||||
</p>
|
||||
|
||||
<div class="cart-mobile-page__scroll">
|
||||
@@ -59,7 +59,6 @@ const emit = defineEmits<{
|
||||
<div class="col-product">
|
||||
<strong>{{ item.productName }}</strong>
|
||||
<span>کد: {{ item.productCode }}</span>
|
||||
<span v-if="item.companyName" class="col-product__company">{{ item.companyName }}</span>
|
||||
</div>
|
||||
<div class="col-meterage">{{ formatNumber(item.meterage) }}</div>
|
||||
<div class="col-delete">
|
||||
@@ -100,7 +99,7 @@ const emit = defineEmits<{
|
||||
</div>
|
||||
</div>
|
||||
<p class="cart-mobile-page__hint">
|
||||
پس از ثبت، همه اقلام در یک پیشسفارش واحد قرار میگیرند و مدیریت قیمتگذاری میکند.
|
||||
پس از ثبت سفارش، کارشناسان قیمتگذاری میکنند و فاکتور برای شما صادر میشود.
|
||||
</p>
|
||||
<BaseButton block :loading="submitting" @click="emit('checkout')">ثبت پیشسفارش</BaseButton>
|
||||
</div>
|
||||
@@ -174,7 +173,7 @@ const emit = defineEmits<{
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.cart-mobile-page__company {
|
||||
.cart-mobile-page__meta {
|
||||
margin: 0 0 16px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
@@ -50,7 +50,7 @@ function getProductCount(catalogId: string) {
|
||||
subtitle="کالیته مورد نظر را انتخاب کنید"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/companies')"
|
||||
@back="router.push('/catalogs')"
|
||||
/>
|
||||
|
||||
<header class="catalog-list-page__desk-head desktop-only">
|
||||
|
||||
@@ -82,8 +82,14 @@ async function submit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<PageHeader title="درخواست کالیته" show-back centered @back="router.back()" />
|
||||
<div class="page catalog-request-page">
|
||||
<PageHeader class="mobile-only" title="درخواست کالیته" show-back centered @back="router.back()" />
|
||||
|
||||
<header class="desktop-page__head desktop-only">
|
||||
<h1 class="desktop-page__title">درخواست کالیته</h1>
|
||||
<p class="desktop-page__subtitle">محصولات و تعداد اقلام موردنظر را مشخص کنید</p>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav body">
|
||||
<p class="hint">
|
||||
{{ preselectId ? 'این محصول برای درخواست کالیته انتخاب شده است. در صورت نیاز اقلام دیگر را هم اضافه کنید.' : 'محصولات و تعداد اقلام موردنظر را مشخص کنید.' }}
|
||||
@@ -91,35 +97,60 @@ async function submit() {
|
||||
<PageSkeleton v-if="loading" variant="form" />
|
||||
<p v-else-if="error" class="error">{{ error }}</p>
|
||||
<template v-else>
|
||||
<article
|
||||
v-for="item in products"
|
||||
:key="item.id"
|
||||
class="row"
|
||||
:class="{ 'row--selected': preselectId === item.id }"
|
||||
>
|
||||
<div>
|
||||
<strong>{{ item.name }}</strong>
|
||||
<span>کد {{ item.code }}</span>
|
||||
<div class="request-layout">
|
||||
<div class="request-products">
|
||||
<article
|
||||
v-for="item in products"
|
||||
:key="item.id"
|
||||
class="row"
|
||||
:class="{ 'row--selected': preselectId === item.id }"
|
||||
>
|
||||
<div>
|
||||
<strong>{{ item.name }}</strong>
|
||||
<span>کد {{ item.code }}</span>
|
||||
</div>
|
||||
<input
|
||||
class="qty"
|
||||
type="number"
|
||||
min="0"
|
||||
:value="quantities[item.id] ?? 0"
|
||||
@input="setQty(item.id, Number(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
<input
|
||||
class="qty"
|
||||
type="number"
|
||||
min="0"
|
||||
:value="quantities[item.id] ?? 0"
|
||||
@input="setQty(item.id, Number(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</article>
|
||||
<label class="note">
|
||||
توضیحات
|
||||
<textarea v-model="note" rows="3" placeholder="آدرس یا توضیحات اختیاری" />
|
||||
</label>
|
||||
<p class="summary">اقلام انتخابشده: {{ formatNumber(selectedItems.length) }}</p>
|
||||
<button type="button" class="submit" :disabled="saving" @click="submit">
|
||||
{{ saving ? 'در حال ثبت...' : 'ثبت درخواست کالیته' }}
|
||||
</button>
|
||||
<button type="button" class="linkish" @click="router.push('/catalog-requests')">
|
||||
وضعیت درخواستهای قبلی
|
||||
</button>
|
||||
|
||||
<aside class="request-sidebar desktop-summary-panel">
|
||||
<h2 class="desktop-summary-panel__title">خلاصه درخواست</h2>
|
||||
<div class="desktop-summary-panel__row">
|
||||
<span>اقلام انتخابشده</span>
|
||||
<strong>{{ formatNumber(selectedItems.length) }}</strong>
|
||||
</div>
|
||||
<label class="note">
|
||||
توضیحات
|
||||
<textarea v-model="note" rows="3" placeholder="آدرس یا توضیحات اختیاری" />
|
||||
</label>
|
||||
<button type="button" class="submit" :disabled="saving" @click="submit">
|
||||
{{ saving ? 'در حال ثبت...' : 'ثبت درخواست کالیته' }}
|
||||
</button>
|
||||
<button type="button" class="linkish" @click="router.push('/catalog-requests')">
|
||||
وضعیت درخواستهای قبلی
|
||||
</button>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="request-mobile-footer mobile-only">
|
||||
<label class="note">
|
||||
توضیحات
|
||||
<textarea v-model="note" rows="3" placeholder="آدرس یا توضیحات اختیاری" />
|
||||
</label>
|
||||
<p class="summary">اقلام انتخابشده: {{ formatNumber(selectedItems.length) }}</p>
|
||||
<button type="button" class="submit" :disabled="saving" @click="submit">
|
||||
{{ saving ? 'در حال ثبت...' : 'ثبت درخواست کالیته' }}
|
||||
</button>
|
||||
<button type="button" class="linkish" @click="router.push('/catalog-requests')">
|
||||
وضعیت درخواستهای قبلی
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,4 +254,61 @@ async function submit() {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.request-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.request-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.request-mobile-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.catalog-request-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 20px var(--page-padding) 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.request-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.request-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.request-sidebar .submit {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.request-mobile-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
box-shadow: var(--shadow-card);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,9 +12,23 @@ onMounted(() => store.hydrate())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<PageHeader title="وضعیت کالیتهها" show-back centered @back="router.push('/catalogs')" />
|
||||
<div class="page catalog-requests-page">
|
||||
<PageHeader class="mobile-only" title="وضعیت کالیتهها" show-back centered @back="router.push('/catalogs')" />
|
||||
|
||||
<header class="desktop-page__head desktop-only catalog-requests-desk-head">
|
||||
<div>
|
||||
<h1 class="desktop-page__title">وضعیت کالیتهها</h1>
|
||||
<p class="desktop-page__subtitle">پیگیری درخواستهای کالیته ثبتشده</p>
|
||||
</div>
|
||||
<button type="button" class="desk-new-btn" @click="router.push('/catalog-request')">
|
||||
درخواست کالیته جدید
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav body">
|
||||
<button type="button" class="new-btn mobile-only" @click="router.push('/catalog-request')">
|
||||
درخواست کالیته جدید
|
||||
</button>
|
||||
<button type="button" class="new-btn" @click="router.push('/catalog-request')">
|
||||
درخواست کالیته جدید
|
||||
</button>
|
||||
@@ -100,4 +114,57 @@ onMounted(() => store.hydrate())
|
||||
.note {
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
|
||||
.catalog-requests-desk-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.desk-new-btn {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
padding: 10px 16px;
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.catalog-requests-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 20px var(--page-padding) 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.empty {
|
||||
grid-column: 1 / -1;
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: var(--shadow-card);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.body {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
filterCatalogGallery,
|
||||
type CatalogGalleryFilter,
|
||||
} from '@/utils/catalogGallery'
|
||||
import { MOCK_CALITE_CARDS } from '@/data/caliteCollections'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -27,8 +28,8 @@ const crm = useCrmStore()
|
||||
const catalogs = ref<Catalog[]>([])
|
||||
const products = ref<CatalogProductCard[]>([])
|
||||
const loading = ref(true)
|
||||
const searchOpen = ref(false)
|
||||
const search = ref('')
|
||||
const searchFieldRef = ref<InstanceType<typeof SearchField> | null>(null)
|
||||
const filter = ref<CatalogGalleryFilter>('all')
|
||||
|
||||
const filters: { id: CatalogGalleryFilter; label: string }[] = [
|
||||
@@ -38,7 +39,10 @@ const filters: { id: CatalogGalleryFilter; label: string }[] = [
|
||||
{ id: 'season', label: 'بر اساس فصل' },
|
||||
]
|
||||
|
||||
const gallery = computed(() => buildCatalogGallery(catalogs.value, products.value))
|
||||
const gallery = computed(() => {
|
||||
const built = buildCatalogGallery(catalogs.value, products.value)
|
||||
return built.length ? built : MOCK_CALITE_CARDS
|
||||
})
|
||||
const cards = computed(() => filterCatalogGallery(gallery.value, filter.value, search.value))
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -65,9 +69,8 @@ function openCatalog(id: string) {
|
||||
router.push({ name: 'catalog-detail', params: { catalogId: id } })
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
searchOpen.value = !searchOpen.value
|
||||
if (!searchOpen.value) search.value = ''
|
||||
function focusSearch() {
|
||||
searchFieldRef.value?.focus()
|
||||
}
|
||||
|
||||
function metaLine(card: (typeof gallery.value)[number]) {
|
||||
@@ -90,13 +93,7 @@ function metaLine(card: (typeof gallery.value)[number]) {
|
||||
<h1>کالیتهها</h1>
|
||||
<div class="calite-hub__nav-end">
|
||||
<AuthThemeToggle variant="inline" class="mobile-only" />
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:aria-label="searchOpen ? 'بستن جستجو' : 'جستجو'"
|
||||
:aria-pressed="searchOpen"
|
||||
@click="toggleSearch"
|
||||
>
|
||||
<button type="button" class="icon-btn mobile-only" aria-label="جستجو" @click="focusSearch">
|
||||
<IconSearch :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -106,10 +103,10 @@ function metaLine(card: (typeof gallery.value)[number]) {
|
||||
<p class="calite-hub__lead">مجموعهای از بهترین پارچهها، در یک نگاه</p>
|
||||
|
||||
<SearchField
|
||||
v-if="searchOpen"
|
||||
:model-value="search"
|
||||
ref="searchFieldRef"
|
||||
v-model="search"
|
||||
class="calite-hub__search"
|
||||
placeholder="جستجوی نام کالیته..."
|
||||
@update:model-value="search = $event"
|
||||
/>
|
||||
|
||||
<div class="calite-hub__chips" role="tablist" aria-label="مرتبسازی کالیتهها">
|
||||
@@ -169,8 +166,8 @@ function metaLine(card: (typeof gallery.value)[number]) {
|
||||
|
||||
<style scoped>
|
||||
.calite-hub {
|
||||
background: #f4f0ed;
|
||||
color: #2a2622;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.calite-hub__nav {
|
||||
@@ -178,7 +175,7 @@ function metaLine(card: (typeof gallery.value)[number]) {
|
||||
grid-template-columns: 40px 1fr auto;
|
||||
align-items: center;
|
||||
padding: 6px 12px 0;
|
||||
background: #f4f0ed;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.calite-hub__nav-end {
|
||||
@@ -203,11 +200,15 @@ function metaLine(card: (typeof gallery.value)[number]) {
|
||||
.calite-hub__lead {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: #8a847c;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.calite-hub__search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -234,10 +235,10 @@ function metaLine(card: (typeof gallery.value)[number]) {
|
||||
flex-shrink: 0;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid #eae6e2;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: #eae6e2;
|
||||
color: #615f5c;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
@@ -349,30 +350,7 @@ function metaLine(card: (typeof gallery.value)[number]) {
|
||||
.calite-hub__empty {
|
||||
margin: 16px 0 0;
|
||||
text-align: center;
|
||||
color: #8a847c;
|
||||
}
|
||||
|
||||
html.dark .calite-hub,
|
||||
html.dark .calite-hub__nav {
|
||||
background: #0b0f14;
|
||||
color: #eef2f8;
|
||||
}
|
||||
|
||||
html.dark .calite-hub__lead,
|
||||
html.dark .calite-hub__empty {
|
||||
color: #9aa8bc;
|
||||
}
|
||||
|
||||
html.dark .calite-chip {
|
||||
border-color: #2a3444;
|
||||
background: #1a2230;
|
||||
color: #c5d0de;
|
||||
}
|
||||
|
||||
html.dark .calite-chip.is-on {
|
||||
border-color: #e8e4df;
|
||||
background: #e8e4df;
|
||||
color: #121821;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
html.dark .calite-card {
|
||||
@@ -416,7 +394,11 @@ html.dark .calite-card__copy small {
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
min-height: auto;
|
||||
background: #f4f0ed;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.calite-hub__nav {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.calite-hub__scroll {
|
||||
|
||||
@@ -50,7 +50,7 @@ function openSeason(id: string) {
|
||||
|
||||
<template>
|
||||
<div class="page categories-page">
|
||||
<header class="cat-top">
|
||||
<header class="cat-top mobile-only">
|
||||
<button type="button" class="icon-btn" aria-label="بازگشت" @click="goBack">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
@@ -66,6 +66,11 @@ function openSeason(id: string) {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<header class="desktop-page__head desktop-only">
|
||||
<h1 class="desktop-page__title">دستهبندیها</h1>
|
||||
<p class="desktop-page__subtitle">دسته موردنظر خود را انتخاب کنید و محصولات را ببینید</p>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav cat-scroll">
|
||||
<p v-if="error">{{ error }}</p>
|
||||
<PageSkeleton v-else-if="loading" variant="grid" />
|
||||
@@ -502,4 +507,74 @@ function openSeason(id: string) {
|
||||
line-height: 1.35;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.categories-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
padding: 8px var(--page-padding) 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cat-scroll {
|
||||
padding: 8px 0 28px;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.block-sub {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.usage-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.usage-card__body strong {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.usage-card__link {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.season-stack {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.season-card {
|
||||
min-height: 156px;
|
||||
}
|
||||
|
||||
.feature-row {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: 14px 12px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.feature-card__text strong {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.feature-card__text span {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.usage-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,6 +23,7 @@ import IconPhone from '@/components/icons/IconPhone.vue'
|
||||
import IconMail from '@/components/icons/IconMail.vue'
|
||||
import IconEditPen from '@/components/icons/IconEditPen.vue'
|
||||
import MeetingReportOverlay from '@/components/customers/MeetingReportOverlay.vue'
|
||||
import CustomerCreateOverlay from '@/components/customers/CustomerCreateOverlay.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -31,6 +32,7 @@ const auth = useAuthStore()
|
||||
const id = computed(() => String(route.query.id ?? ''))
|
||||
const tab = computed(() => (String(route.query.tab ?? 'info') as CustomerTab))
|
||||
const showMeetingReport = computed(() => route.query.meetingReport === '1')
|
||||
const showEdit = computed(() => route.query.edit === '1')
|
||||
const customer = ref<CrmCustomer | null>(null)
|
||||
const orders = ref<CrmOrderSummary[]>([])
|
||||
const meetings = ref<CrmMeeting[]>([])
|
||||
@@ -91,6 +93,47 @@ async function onMeetingReportSuccess() {
|
||||
await load()
|
||||
}
|
||||
|
||||
function openEdit() {
|
||||
router.push({
|
||||
path: '/customer',
|
||||
query: { id: id.value, tab: tab.value, edit: '1' },
|
||||
})
|
||||
}
|
||||
|
||||
function closeEdit() {
|
||||
router.replace({
|
||||
path: '/customer',
|
||||
query: { id: id.value, tab: tab.value },
|
||||
})
|
||||
}
|
||||
|
||||
async function onEditSuccess() {
|
||||
closeEdit()
|
||||
await load()
|
||||
}
|
||||
|
||||
const editInitial = computed(() => {
|
||||
if (!customer.value) return null
|
||||
const c = customer.value
|
||||
if (c.firstName && c.lastName) {
|
||||
return {
|
||||
firstName: c.firstName,
|
||||
lastName: c.lastName,
|
||||
phone: c.phone,
|
||||
businessName: c.businessName ?? '',
|
||||
}
|
||||
}
|
||||
const parts = c.contactName.trim().split(/\s+/)
|
||||
const firstName = parts[0] ?? ''
|
||||
const lastName = parts.slice(1).join(' ') || firstName
|
||||
return {
|
||||
firstName,
|
||||
lastName,
|
||||
phone: c.phone,
|
||||
businessName: c.name !== c.contactName ? c.name : '',
|
||||
}
|
||||
})
|
||||
|
||||
function typeLabel(type: MeetingInteractionType) {
|
||||
if (type === 'in_person') return 'ملاقات حضوری'
|
||||
if (type === 'call') return 'تماس'
|
||||
@@ -158,16 +201,27 @@ function customerStatusClass(status: CustomerStatus) {
|
||||
<template>
|
||||
<div v-if="customer" class="page customer-page">
|
||||
<PageHeader
|
||||
class="mobile-only"
|
||||
:title="tab === 'reports' ? 'پروفایل مشتری' : 'جزئیات مشتری'"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/customers')"
|
||||
>
|
||||
<template v-if="tab === 'info'" #trailing>
|
||||
<button type="button" class="edit-btn" @click="router.back()">ویرایش</button>
|
||||
<button type="button" class="edit-btn" @click="openEdit">ویرایش</button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<header class="desktop-page__head desktop-only customer-desk-head">
|
||||
<div>
|
||||
<h1 class="desktop-page__title">{{ customer.name }}</h1>
|
||||
<p class="desktop-page__subtitle" dir="ltr">{{ customer.phone }}</p>
|
||||
</div>
|
||||
<button v-if="tab === 'info'" type="button" class="edit-btn edit-btn--desk" @click="openEdit">
|
||||
ویرایش
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav customer-scroll">
|
||||
<section v-if="tab !== 'reports'" class="profile-card">
|
||||
<div class="profile-card__avatar" :style="{ background: customer.avatarColor }">
|
||||
@@ -377,6 +431,14 @@ function customerStatusClass(status: CustomerStatus) {
|
||||
@close="closeMeetingReport"
|
||||
@success="onMeetingReportSuccess"
|
||||
/>
|
||||
|
||||
<CustomerCreateOverlay
|
||||
:open="showEdit"
|
||||
:customer-id="id"
|
||||
:initial="editInitial"
|
||||
@close="closeEdit"
|
||||
@success="onEditSuccess"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -918,4 +980,76 @@ function customerStatusClass(status: CustomerStatus) {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.customer-desk-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.edit-btn--desk {
|
||||
border-radius: 12px;
|
||||
padding: 10px 16px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.customer-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 20px var(--page-padding) 28px;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 300px) minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.customer-desk-head {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.customer-scroll {
|
||||
display: contents;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.profile-card,
|
||||
.profile-banner {
|
||||
grid-column: 1;
|
||||
grid-row: 2 / span 12;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-card);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.purchase-card,
|
||||
.panel-card {
|
||||
grid-column: 2;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px 20px;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.cta-bar {
|
||||
grid-column: 2;
|
||||
position: static;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -118,7 +118,7 @@ function cycleStatusFilter() {
|
||||
|
||||
<template>
|
||||
<div class="page customers-page">
|
||||
<PageHeader title="مشتریان من" show-back centered @back="router.back()">
|
||||
<PageHeader class="mobile-only" title="مشتریان من" show-back centered @back="router.back()">
|
||||
<template #trailing>
|
||||
<button
|
||||
type="button"
|
||||
@@ -131,6 +131,17 @@ function cycleStatusFilter() {
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<header class="desktop-page__head desktop-only customers-desk-head">
|
||||
<div>
|
||||
<h1 class="desktop-page__title">مشتریان من</h1>
|
||||
<p class="desktop-page__subtitle">مدیریت مشتریان و پیگیری خریدها</p>
|
||||
</div>
|
||||
<button type="button" class="desk-add-btn" @click="onRegisterCustomer">
|
||||
<IconPlus :size="18" />
|
||||
ثبت مشتری
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav customers-scroll">
|
||||
<div class="toolbar">
|
||||
<SearchField
|
||||
@@ -454,4 +465,84 @@ function cycleStatusFilter() {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.customers-desk-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.desk-add-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
padding: 10px 16px;
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.customers-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 20px var(--page-padding) 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.customers-scroll {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
grid-template-columns: minmax(280px, 420px) auto;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
width: auto;
|
||||
padding: 0 14px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.customer-card {
|
||||
box-shadow: var(--shadow-card);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.customer-card .chevron {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.customers-scroll > .customer-card {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.customers-scroll {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.toolbar,
|
||||
.summary-card,
|
||||
.tabs,
|
||||
.promo,
|
||||
.customers-scroll > p,
|
||||
.customers-scroll > :deep(.page-skeleton) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -305,7 +305,7 @@ async function submitReceipt() {
|
||||
</div>
|
||||
|
||||
<div class="order-info-card__row">
|
||||
<span class="order-info-card__key">شرکت</span>
|
||||
<span class="order-info-card__key">تأمینکننده</span>
|
||||
<span class="order-info-card__value order-info-card__value--accent">{{ order.companyName }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ const order = computed(() => orderStore.getOrderById(route.params.id as string))
|
||||
<strong>{{ order.orderNumber }}</strong>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span>شرکت:</span>
|
||||
<span>تأمینکننده:</span>
|
||||
<span>{{ order.companyName }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
|
||||
@@ -100,12 +100,6 @@ const emit = defineEmits<{
|
||||
</div>
|
||||
|
||||
<div class="desktop-toolbar">
|
||||
<div class="desktop-toolbar__actions">
|
||||
<button type="button" class="orders-toolbar__filter-btn">
|
||||
<IconFilter :size="18" />
|
||||
فیلترها
|
||||
</button>
|
||||
</div>
|
||||
<div class="orders-toolbar__pills" role="tablist" aria-label="فیلتر وضعیت">
|
||||
<button
|
||||
v-for="pill in desktopFilterTabs"
|
||||
|
||||
@@ -37,8 +37,14 @@ async function remove(id: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<PageHeader title="علاقهمندیها" show-back centered @back="router.back()" />
|
||||
<div class="page favorites-page">
|
||||
<PageHeader class="mobile-only" title="علاقهمندیها" show-back centered @back="router.back()" />
|
||||
|
||||
<header class="desktop-page__head desktop-only">
|
||||
<h1 class="desktop-page__title">علاقهمندیها</h1>
|
||||
<p class="desktop-page__subtitle">محصولات ذخیرهشده برای دسترسی سریع</p>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav body">
|
||||
<PageSkeleton v-if="loading" variant="grid" />
|
||||
<p v-else-if="error">{{ error }}</p>
|
||||
@@ -61,7 +67,7 @@ async function remove(id: string) {
|
||||
>
|
||||
<IconHeart :size="18" filled color="#0a353c" />
|
||||
</button>
|
||||
<IconChevronLeft :size="16" color="#9aa3a0" />
|
||||
<IconChevronLeft class="row__chevron" :size="16" color="#9aa3a0" />
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,4 +120,40 @@ async function remove(id: string) {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.favorites-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 20px var(--page-padding) 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
grid-column: 1 / -1;
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
box-shadow: var(--shadow-card);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.row__chevron {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.body {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -211,8 +211,8 @@ function shareProduct() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="product" class="page product-detail">
|
||||
<header class="pd-header">
|
||||
<div v-if="product" class="page product-detail product-page">
|
||||
<header class="pd-header mobile-only">
|
||||
<button type="button" class="icon-btn" aria-label="بازگشت" @click="goBack">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
@@ -240,7 +240,38 @@ function shareProduct() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll pd-scroll">
|
||||
<header class="desktop-page__head desktop-only pd-desk-head">
|
||||
<div>
|
||||
<h1 class="desktop-page__title">{{ product.name }}</h1>
|
||||
<p class="desktop-page__subtitle">کد محصول: {{ product.code }}</p>
|
||||
</div>
|
||||
<div class="pd-desk-head__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:aria-label="
|
||||
favorites.isFavorite(product.id)
|
||||
? 'حذف از علاقهمندیها'
|
||||
: 'افزودن به علاقهمندیها'
|
||||
"
|
||||
:aria-pressed="favorites.isFavorite(product.id)"
|
||||
@click="toggleFavorite"
|
||||
>
|
||||
<IconHeart
|
||||
:size="20"
|
||||
:filled="favorites.isFavorite(product.id)"
|
||||
:color="favorites.isFavorite(product.id) ? '#0a353c' : undefined"
|
||||
/>
|
||||
</button>
|
||||
<button type="button" class="icon-btn" aria-label="اشتراکگذاری" @click="shareProduct">
|
||||
<IconShare :size="20" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll pd-scroll product-page__scroll">
|
||||
<div class="product-page__layout">
|
||||
<div class="product-page__main">
|
||||
<div class="gallery">
|
||||
<div
|
||||
class="gallery__main"
|
||||
@@ -368,9 +399,39 @@ function shareProduct() {
|
||||
<div class="roll-field">{{ formatNumber(product.rollMeters) }} متر</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="product-page__sidebar desktop-only">
|
||||
<div class="cart-summary-panel">
|
||||
<h2 class="cart-summary-panel__title">ثبت سفارش</h2>
|
||||
<div class="cart-summary-panel__row">
|
||||
<span>متراژ سفارش</span>
|
||||
<strong>{{ formatNumber(orderMeters) }} متر</strong>
|
||||
</div>
|
||||
<div class="cart-summary-panel__row">
|
||||
<span>حداقل سفارش</span>
|
||||
<strong>{{ formatNumber(minRolls) }} طاقه</strong>
|
||||
</div>
|
||||
<div class="cart-summary-panel__btn">
|
||||
<button type="button" class="btn-primary" :disabled="adding" @click="addToOrder">
|
||||
<IconCart :size="18" color="#ffffff" />
|
||||
{{ adding ? 'در حال افزودن...' : 'افزودن به سفارش' }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary pd-desk-catalog-btn"
|
||||
@click="router.push({ path: '/catalog-request', query: { productId: product.id } })"
|
||||
>
|
||||
<img class="btn-secondary__icon" :src="quickCatalogRequestIcon" alt="" aria-hidden="true" />
|
||||
درخواست کالیته
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="pd-footer">
|
||||
<footer class="pd-footer mobile-only sticky-footer">
|
||||
<div class="pd-footer__primary">
|
||||
<button type="button" class="btn-primary" :disabled="adding" @click="addToOrder">
|
||||
<IconCart :size="18" color="#ffffff" />
|
||||
@@ -810,4 +871,83 @@ button.tag--link:hover {
|
||||
padding: 40px var(--page-padding);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pd-desk-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding-inline: var(--page-padding);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pd-desk-head__actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pd-desk-catalog-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.product-detail {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
padding: 8px var(--page-padding) 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pd-desk-head {
|
||||
padding-inline: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pd-scroll {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.gallery__main {
|
||||
height: 360px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gallery__thumbs {
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.pd-body {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.product-page__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.product-page__sidebar {
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.cart-summary-panel__btn .btn-primary {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.product-page__layout {
|
||||
grid-template-columns: minmax(0, 1fr) 340px;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.gallery__main {
|
||||
height: 420px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -180,12 +180,6 @@ onMounted(async () => {
|
||||
<button type="button" class="hero__desk-btn" @click="router.push('/profile/edit')">
|
||||
ویرایش اطلاعات
|
||||
</button>
|
||||
<button type="button" class="hero__desk-btn" @click="router.push('/profile/checks')">
|
||||
لیست چکها
|
||||
</button>
|
||||
<button type="button" class="hero__desk-btn" @click="router.push('/profile/display')">
|
||||
تنظیمات
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -36,10 +36,6 @@ function goBack() {
|
||||
<!-- Desktop -->
|
||||
<div class="desktop-page profile-subpage-desktop desktop-only">
|
||||
<header class="desktop-page__head profile-subpage-desktop__head">
|
||||
<button type="button" class="profile-subpage-desktop__back" @click="goBack">
|
||||
<IconChevronBack :size="18" />
|
||||
بازگشت به پروفایل
|
||||
</button>
|
||||
<h1 class="desktop-page__title">{{ title }}</h1>
|
||||
<p v-if="subtitle" class="desktop-page__subtitle">{{ subtitle }}</p>
|
||||
</header>
|
||||
@@ -57,6 +53,9 @@ function goBack() {
|
||||
|
||||
.profile-subpage__card {
|
||||
padding: 12px 16px 20px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.profile-subpage__nav {
|
||||
@@ -71,7 +70,7 @@ function goBack() {
|
||||
padding: 8px 12px;
|
||||
background: var(--color-elevated);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -115,25 +114,6 @@ function goBack() {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.profile-subpage-desktop__back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile-subpage-desktop__back:hover {
|
||||
color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.profile-subpage-desktop__panel {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
|
||||
@@ -185,7 +185,7 @@ function toggleCompleted() {
|
||||
|
||||
<template>
|
||||
<div class="page reminders-page">
|
||||
<header class="top">
|
||||
<header class="top mobile-only">
|
||||
<button type="button" class="icon-btn" aria-label="بازگشت" @click="goBack">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
@@ -200,6 +200,27 @@ function toggleCompleted() {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<header class="desktop-page__head desktop-only reminders-desk-head">
|
||||
<div>
|
||||
<h1 class="desktop-page__title">یادآورها</h1>
|
||||
<p class="desktop-page__subtitle">پیگیری و مدیریت یادآورهای مشتریان</p>
|
||||
</div>
|
||||
<div class="reminders-desk-head__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="desk-icon-btn"
|
||||
aria-label="اعلانها"
|
||||
@click="router.push('/profile/notifications')"
|
||||
>
|
||||
<IconBell :size="20" />
|
||||
</button>
|
||||
<button type="button" class="desk-new-btn" @click="openCreate">
|
||||
<IconPlus :size="18" color="#fff" />
|
||||
یادآور جدید
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav reminders-scroll">
|
||||
<div class="tabs" role="tablist" aria-label="فیلتر یادآورها">
|
||||
<button
|
||||
@@ -408,7 +429,7 @@ function toggleCompleted() {
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<button type="button" class="new-btn" @click="openCreate">
|
||||
<button type="button" class="new-btn mobile-only" @click="openCreate">
|
||||
<IconPlus :size="20" color="#fff" />
|
||||
یادآور جدید
|
||||
</button>
|
||||
@@ -1013,4 +1034,72 @@ function toggleCompleted() {
|
||||
color: #b42318;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.reminders-desk-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.reminders-desk-head__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desk-icon-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.desk-new-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
padding: 10px 16px;
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.reminders-page {
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
padding: 20px var(--page-padding) 28px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.reminders-scroll {
|
||||
padding: 0;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.reminder-card {
|
||||
box-shadow: var(--shadow-card);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.create-panel {
|
||||
max-width: 560px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user