feat: move brand name to one config and redesign the accounting card

اپ: نام برند در همه‌ی صفحه‌ها و پیام‌ها یکسان شده و از یک تنظیم مرکزی خوانده می‌شود.
اپ: کارت حسابداری در صفحه‌ی مشتری بازطراحی شد؛ مانده‌ها و وضعیت لینک واضح‌تر نمایش داده می‌شوند.
پنل: کارت حسابداری در پرونده‌ی مشتری بازطراحی شد؛ مانده‌ها، آخرین ارسال و فرم لینک دستی واضح‌تر شده‌اند.
پنل: ستون «کد» در جدول‌ها به‌طور پیش‌فرض پنهان است و از انتخاب ستون‌ها قابل نمایش است.
پنل: تغییر نمایش ستون‌ها در جدول‌ها بلافاصله اعمال می‌شود.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-14 13:31:20 +03:30
co-authored by Claude Haiku 5.5
parent 5d2a1efb0c
commit 532ceee189
100 changed files with 1870 additions and 579 deletions
@@ -289,7 +289,7 @@ const THEMES: { value: ThemeChoice; label: string; icon: string }[] = [
}
.ap__theme.is-on {
color: @ink;
box-shadow: inset 0 0 0 1.5px @ink;
box-shadow: inset 0 0 0 1.5px @wine;
}
.ap__preview {
@@ -306,8 +306,8 @@ const THEMES: { value: ThemeChoice; label: string; icon: string }[] = [
width: 22px;
height: 22px;
border-radius: 50%;
color: @onInk;
background: @ink;
color: #fff;
background: @wine;
}
.ap__row {
@@ -222,7 +222,7 @@
v-model:show="data.notify"
art="logout"
title="آیا می خواهید از برنامه خارج شوید؟"
description="برای استفاده دوباره از امکانات منتزر، باید دوباره وارد حساب کاربری خود شوید."
:description="`برای استفاده دوباره از امکانات ${brand.name}، باید دوباره وارد حساب کاربری خود شوید.`"
confirm-text="بله"
cancel-text="بی خیال"
@confirm="logOut"
@@ -232,7 +232,7 @@
v-model:show="data.loginNotify"
art="login"
title="ابتدا وارد حساب کاربری شوید"
description="برای دسترسی به تمامی امکانات نرم افزار منتزر، گزینه ورود/ثبت نام را لمس کنید و در صورت نداشتن حساب کاربری، ثبت نام نمایید."
:description="`برای دسترسی به تمامی امکانات نرم افزار ${brand.name}، گزینه ورود/ثبت نام را لمس کنید و در صورت نداشتن حساب کاربری، ثبت نام نمایید.`"
confirm-text="ورود/ثبت نام"
cancel-text="بیخیال"
@confirm="router.push({ name: 'login' })"
@@ -241,6 +241,7 @@
</template>
<script setup lang="ts">
import { brand } from '../../brand'
import { computed, nextTick, onActivated, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Loading, PullRefresh } from 'vant'
@@ -453,9 +454,9 @@ onMounted(async () => {
margin-bottom: 28px;
padding: 20px 18px 16px;
border-radius: 26px;
background: linear-gradient(155deg, @heroSurface 0%, @heroSurface 55%, #5E1427 130%);
background: linear-gradient(155deg, @heroSurface 0%, @heroSurface 55%, #5A0027 130%);
color: #fff;
box-shadow: 0 22px 40px -26px rgba(20, 22, 26, 0.32);
box-shadow: 0 22px 40px -26px rgba(124, 0, 53, 0.32);
}
.dash__heroTop {
@@ -125,7 +125,7 @@ watch(() => favorites.ids.join(','), load)
font-family: inherit;
font-size: 12.5px;
font-weight: 700;
color: @onInk;
background: @ink;
color: #fff;
background: @wine;
}
</style>
@@ -26,7 +26,7 @@
<div class="auth__mark" aria-hidden="true">
<Icon icon="vuesax-linear:security-user" size="28" />
</div>
<h2 class="auth__title">ورود به منتزر</h2>
<h2 class="auth__title">ورود به {{ brand.name }}</h2>
<p class="auth__sub">
{{ body.step === 1 ? 'شماره موبایل خود را وارد کنید' : 'رمز یکبارمصرف را وارد کنید' }}
</p>
@@ -81,6 +81,7 @@
</template>
<script setup lang="ts">
import { brand } from '../../brand'
import { computed, nextTick, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Form, FormItem, Input } from 'ant-design-vue'
@@ -147,7 +148,7 @@ const sendPhone = async () => {
// this branch the person is told "خطا در ارسال کد"، which is the
// very confusion the 404 was added to remove.
const text =
e instanceof Error && /\((?:429|404)\)/.test(e.message) ? extractServerMessage(e) : null
e instanceof Error && /\((?:429|404|403)\)/.test(e.message) ? extractServerMessage(e) : null
toast.error(text ?? 'خطا در ارسال کد. لطفاً دوباره تلاش کنید.')
} finally {
body.loading = false
@@ -294,8 +295,8 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
font-family: inherit;
font-size: 13px;
font-weight: 700;
color: @onInk;
background: @ink;
color: #fff;
background: @wine;
transition: transform 0.15s ease, opacity 0.15s ease;
}
@@ -347,7 +348,7 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
.otp-input:focus {
outline: none;
border-color: @ink;
border-color: @wine;
}
.otp-input.error {
@@ -298,9 +298,9 @@ onActivated(() => {
}
.ord__chip.is-on {
color: @onInk;
background: @ink;
border-color: @ink;
color: #fff;
background: @wine;
border-color: @wine;
}
.ord__chip:active {
@@ -430,15 +430,15 @@ onActivated(() => {
}
.detail__step.is-done .detail__stepDot {
color: @onInk;
background: @ink;
border-color: @ink;
color: #fff;
background: @wine;
border-color: @wine;
}
.detail__step.is-active .detail__stepDot {
color: #fff;
background: @wine;
border-color: @wine;
background: @wineDeep;
border-color: @wineDeep;
}
.detail__stepTitle {
@@ -16,10 +16,9 @@
drops the leading 0 and shows spinners), and Persian/Arabic digits are
converted to Latin as the user types, so the validators accept them.
STUBBED, unchanged from before: legacy's POST /major/register has no
counterpart in apps/api yet. Submitting shows the "awaiting review" state
without persisting anything — the copy says the request is under review,
which is what legacy said too, so it does not claim an account exists.
Submits to POST customer/auth/register (legacy POST /major/register). The new
customer is created inactive and lands in the admin's «مشتریان در انتظار»
list; they can sign in once an admin approves them.
-->
<template>
<div class="reg">
@@ -192,6 +191,8 @@ import { useRouter } from 'vue-router'
import { Form, FormItem, Input, Textarea } from 'ant-design-vue'
import Icon from '../../components/icon/Icon.vue'
import PageBar from '../../components/ui/PageBar.vue'
import { api } from '../../api/client'
import { toast } from '../../composables/useToast'
const emit = defineEmits<{ (e: 'register', value: boolean): void }>()
const router = useRouter()
@@ -235,14 +236,35 @@ async function onFinish() {
}
try {
loading.value = true
// Stubbed — no create-customer endpoint exists in apps/api yet.
await new Promise((r) => setTimeout(r, 600))
await api.register({
fname: String(formModel.fname ?? ''),
lname: String(formModel.lname ?? ''),
mobile: String(formModel.mobile ?? ''),
store_name: String(formModel.store_name ?? ''),
phone: String(formModel.phone ?? ''),
address: String(formModel.address ?? ''),
city: String(formModel.city ?? ''),
})
done.value = true
window.scrollTo({ top: 0 })
} catch (e: unknown) {
toast.error(serverMessage(e) ?? 'خطا در ثبت اطلاعات. لطفاً دوباره تلاش کنید.')
} finally {
loading.value = false
}
}
/** The API's Persian `message` (duplicate number, bad mobile, throttle), if any. */
function serverMessage(e: unknown): string | null {
const match = e instanceof Error ? e.message.match(/\{.*\}/) : null
if (!match) return null
try {
const parsed = JSON.parse(match[0]) as { message?: string | string[] }
return Array.isArray(parsed.message) ? parsed.message[0] ?? null : parsed.message ?? null
} catch {
return null
}
}
</script>
<style scoped lang="less">
@@ -364,16 +386,19 @@ async function onFinish() {
}
}
// Same field language as the rest of the app (SearchInput, login): @canvas
// fill, 1px @hairline edge, and on focus only the edge turns @wineSoft —
// no tinted fill, no glow ring.
:deep(.ant-input),
:deep(.ant-input-affix-wrapper) {
border-radius: 14px;
border: 1.5px solid @hairline;
background: @canvasSoft;
border: 1px solid @hairline;
background: @canvas;
font-family: inherit;
font-size: 14px;
color: @ink;
box-shadow: none;
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
transition: border-color 0.18s ease;
}
:deep(.ant-input) {
@@ -386,9 +411,10 @@ async function onFinish() {
padding: 0 14px;
.ant-input {
min-height: 46px;
min-height: 48px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
@@ -403,7 +429,7 @@ async function onFinish() {
:deep(.ant-input-prefix) {
margin-inline-end: 10px;
color: @inkFaint;
transition: color 0.2s ease;
transition: color 0.18s ease;
}
:deep(.ant-input::placeholder) {
@@ -418,29 +444,21 @@ async function onFinish() {
:deep(.ant-input:focus),
:deep(.ant-input-focused),
:deep(.ant-input-affix-wrapper-focused) {
border-color: @wine;
border-color: @wineSoft;
background: @canvas;
box-shadow: 0 0 0 4px alpha(@wine, 10%);
.ant-input-prefix {
color: @wine;
}
box-shadow: none;
}
:deep(.ant-input-affix-wrapper-focused .ant-input-prefix) {
color: @wine;
color: @wineSoft;
}
// Errors
// Errors: red edge only — like focus, no ring.
:deep(.ant-form-item-has-error .ant-input),
:deep(.ant-form-item-has-error .ant-input-affix-wrapper) {
border-color: @red !important;
background: alpha(@red, 3%);
}
:deep(.ant-form-item-has-error .ant-input:focus),
:deep(.ant-form-item-has-error .ant-input-affix-wrapper-focused) {
box-shadow: 0 0 0 4px alpha(@red, 10%) !important;
background: @canvas;
box-shadow: none !important;
}
:deep(.ant-form-item-has-error .ant-input-affix-wrapper .ant-input) {
@@ -481,14 +499,15 @@ async function onFinish() {
padding: 12px 14px;
border-radius: 14px;
background: @wineWash;
color: @wine;
color: @inkSoft;
font-size: 12px;
font-weight: 600;
font-weight: 500;
line-height: 1.8;
:deep(svg) {
flex: 0 0 auto;
margin-top: 2px;
color: @wine;
}
}
@@ -526,9 +545,9 @@ async function onFinish() {
font-family: inherit;
font-size: 14px;
font-weight: 800;
color: @onInk;
background: @ink;
box-shadow: 0 10px 22px -12px alpha(@ink, 70%);
color: #fff;
background: @wine;
box-shadow: 0 10px 22px -12px alpha(@wine, 70%);
transition: transform 0.15s ease, opacity 0.2s ease;
&:active {