diff --git a/apps/business/src/i18n/messages.ts b/apps/business/src/i18n/messages.ts index 39f2eab..0597118 100644 --- a/apps/business/src/i18n/messages.ts +++ b/apps/business/src/i18n/messages.ts @@ -150,6 +150,7 @@ const en = { 'bizProfile.logo.change': 'Change logo', 'bizProfile.logo.lightTheme': 'Logo for light theme', 'bizProfile.logo.darkTheme': 'Logo for dark theme', + 'bizProfile.logo.aspectAria': 'Logo aspect ratio', 'bizProfile.favicon': 'Favicon', 'bizProfile.favicon.upload': 'Upload favicon', 'bizProfile.favicon.hint': 'Square PNG recommended', @@ -2695,6 +2696,7 @@ const fa: Record = { 'bizProfile.logo.change': 'تغییر لوگو', 'bizProfile.logo.lightTheme': 'لوگو در تم روشن', 'bizProfile.logo.darkTheme': 'لوگو در تم تیره', + 'bizProfile.logo.aspectAria': 'نسبت تصویر لوگو', 'bizProfile.favicon': 'فاوآیکن', 'bizProfile.favicon.upload': 'آپلود فاوآیکن', 'bizProfile.favicon.hint': 'PNG مربعی توصیه می‌شود', diff --git a/apps/business/src/pages/BusinessProfilePage.module.css b/apps/business/src/pages/BusinessProfilePage.module.css index 15124dd..b0fe984 100644 --- a/apps/business/src/pages/BusinessProfilePage.module.css +++ b/apps/business/src/pages/BusinessProfilePage.module.css @@ -40,8 +40,30 @@ margin-bottom: 0; } -.logoLightCol, -.logoDarkCol, +.logosBlock { + grid-column: span 12; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.logosPair { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px 16px; + width: 100%; + min-width: 0; +} + +.logoField { + min-width: 0; + margin-bottom: 0; + display: flex; + flex-direction: column; +} + .faviconCol { grid-column: span 12; min-width: 0; @@ -229,12 +251,8 @@ } @media (min-width: 1280px) { - .logoLightCol { - grid-column: span 2; - } - - .logoDarkCol { - grid-column: span 2; + .logosBlock { + grid-column: span 4; } .faviconCol { diff --git a/apps/business/src/pages/BusinessProfilePage.tsx b/apps/business/src/pages/BusinessProfilePage.tsx index e0a1bf8..4dc905a 100644 --- a/apps/business/src/pages/BusinessProfilePage.tsx +++ b/apps/business/src/pages/BusinessProfilePage.tsx @@ -39,6 +39,14 @@ import styles from './BusinessProfilePage.module.css' type AddressDraft = AddressListItem +type LogoAspectId = '1:1' | '3:2' | '16:9' + +const LOGO_ASPECTS: { id: LogoAspectId; aspect: number }[] = [ + { id: '1:1', aspect: 1 }, + { id: '3:2', aspect: 3 / 2 }, + { id: '16:9', aspect: 16 / 9 }, +] + const EMPTY_SOCIAL: BusinessSocialMedia = { whatsapp: '', telegram: '', @@ -82,6 +90,7 @@ export function BusinessProfilePage() { const [logoMediaId, setLogoMediaId] = useState(null) const [logoDark, setLogoDark] = useState(null) const [logoDarkMediaId, setLogoDarkMediaId] = useState(null) + const [logoAspect, setLogoAspect] = useState('1:1') const [favicon, setFavicon] = useState(null) const [faviconMediaId, setFaviconMediaId] = useState(null) const [addresses, setAddresses] = useState([createEmptyAddressItem()]) @@ -94,6 +103,22 @@ export function BusinessProfilePage() { () => flattenBusinessActivityCategories(activityCategories), [activityCategories], ) + const selectedLogoAspect = + LOGO_ASPECTS.find((option) => option.id === logoAspect)?.aspect ?? 1 + + function handleLogoAspectChange(next: LogoAspectId) { + if (next === logoAspect) return + setLogoAspect(next) + // Applied crops are locked to the old ratio — drop them so the user recrops. + if (logo) { + setLogo(null) + setLogoMediaId(null) + } + if (logoDark) { + setLogoDark(null) + setLogoDarkMediaId(null) + } + } const addressLabels = useMemo( () => ({ @@ -378,32 +403,60 @@ export function BusinessProfilePage() {

{t('bizProfile.section.basic')}

-
- - -
+
+
+
+ + +
-
- - +
+ + +
+
+ +
+ {LOGO_ASPECTS.map((option) => ( + + ))} +
@@ -412,6 +465,7 @@ export function BusinessProfilePage() { compact value={favicon} onChange={setFavicon} + aspect={1} outputFormat="png" accept="image/png" uploadLabel={t('bizProfile.favicon.upload')} diff --git a/apps/super-admin/src/pages/BusinessesPage.module.css b/apps/super-admin/src/pages/BusinessesPage.module.css index 39a980b..48048f0 100644 --- a/apps/super-admin/src/pages/BusinessesPage.module.css +++ b/apps/super-admin/src/pages/BusinessesPage.module.css @@ -58,6 +58,43 @@ flex-shrink: 0; } +.lowSmsAlert { + margin: 12px 0 0; + padding: 10px 14px; + border-radius: var(--radius); + font-size: 13px; + line-height: 1.45; + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.28); + color: #b91c1c; +} + +.lowSmsAlertLabel { + font-weight: 600; +} + +.lowSmsAlertLink { + appearance: none; + border: none; + background: none; + padding: 0; + margin: 0; + font: inherit; + color: inherit; + text-decoration: underline; + text-underline-offset: 2px; + cursor: pointer; +} + +.lowSmsAlertLink:hover { + color: #991b1b; +} + +.lowSmsAlertBalance { + font-variant-numeric: tabular-nums; + opacity: 0.9; +} + .field { display: flex; flex-direction: column; @@ -440,6 +477,13 @@ transition: transform 0.15s, box-shadow 0.15s, background 0.15s; } +.btn:disabled { + opacity: 0.55; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + .btnPrimary { color: white; background: linear-gradient(135deg, var(--primary-glow) 0%, var(--primary) 55%, var(--primary-dark) 100%); diff --git a/apps/super-admin/src/pages/BusinessesPage.tsx b/apps/super-admin/src/pages/BusinessesPage.tsx index 190fd5c..7cff0f0 100644 --- a/apps/super-admin/src/pages/BusinessesPage.tsx +++ b/apps/super-admin/src/pages/BusinessesPage.tsx @@ -333,6 +333,7 @@ export function BusinessesPage() { const [draftName, setDraftName] = useState('') const [draftDomain, setDraftDomain] = useState('') const [draftCategory, setDraftCategory] = useState('') + const [draftSort, setDraftSort] = useState<'created' | 'renewal'>('created') const [editOpen, setEditOpen] = useState(false) const [editBusiness, setEditBusiness] = useState(null) @@ -484,7 +485,7 @@ export function BusinessesPage() { return () => { controller.abort() } - }, [page, pageSize, appliedFilters.name, appliedFilters.domain, appliedFilters.category]) + }, [page, pageSize, appliedFilters.name, appliedFilters.domain, appliedFilters.category, appliedFilters.sort]) const totalPages = useMemo(() => { const total = data?.total ?? 0 @@ -509,6 +510,7 @@ export function BusinessesPage() { ...(draftName.trim() ? { name: draftName.trim() } : {}), ...(draftDomain.trim() ? { domain: draftDomain.trim() } : {}), ...(draftCategory.trim() ? { category: draftCategory.trim() } : {}), + ...(draftSort !== 'created' ? { sort: draftSort } : {}), }) } @@ -516,6 +518,7 @@ export function BusinessesPage() { setDraftName('') setDraftDomain('') setDraftCategory('') + setDraftSort('created') setPage(1) setAppliedFilters({}) } @@ -1503,6 +1506,32 @@ export function BusinessesPage() { aria-label="Category" />
+ +
+ +
@@ -1529,6 +1558,43 @@ export function BusinessesPage() {
+ {data?.lowSmsBusinesses && data.lowSmsBusinesses.length > 0 && ( +
+ + Low SMS credits (< {data.lowSmsThreshold ?? 100}): + {' '} + {data.lowSmsBusinesses.map((b, i) => ( + + {i > 0 ? ', ' : ''} + + + {' '} + · {b.smsBalance.toLocaleString('en-US')} + + + ))} +
+ )} +
diff --git a/apps/super-admin/src/services/businessService.ts b/apps/super-admin/src/services/businessService.ts index 831a820..42a38e4 100644 --- a/apps/super-admin/src/services/businessService.ts +++ b/apps/super-admin/src/services/businessService.ts @@ -15,6 +15,8 @@ export interface ListBusinessesParams { name?: string domain?: string category?: string + /** Default: created (newest first). renewal = nearest next/overdue renewal. */ + sort?: 'created' | 'renewal' } export interface UpdateBusinessPayload { @@ -61,6 +63,7 @@ export async function listBusinesses(params: ListBusinessesParams, signal?: Abor if (params.name) q.set('name', params.name) if (params.domain) q.set('domain', params.domain) if (params.category) q.set('category', params.category) + if (params.sort && params.sort !== 'created') q.set('sort', params.sort) const query = q.toString() const path = `/businesses${query ? `?${query}` : ''}` diff --git a/apps/super-admin/src/types/business.ts b/apps/super-admin/src/types/business.ts index b8ed392..2aa09b7 100644 --- a/apps/super-admin/src/types/business.ts +++ b/apps/super-admin/src/types/business.ts @@ -137,5 +137,13 @@ export interface BusinessesListResponse { total: number page: number pageSize: number + /** Businesses with prepaid SMS credits below lowSmsThreshold (all pages). */ + lowSmsBusinesses?: Array<{ + id: string + name: string + domain: string | null + smsBalance: number + }> + lowSmsThreshold?: number }