From 481f07b31aa3faddcd922c2f0033570ae970147f Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sun, 24 May 2026 00:48:06 +0700 Subject: [PATCH] Refactor TPOS shop and POS routes for parity --- .../apps/tpos-mvp-next/src/app/about/page.tsx | 5 + .../src/app/auth/[...path]/page.tsx | 2 + .../tpos-mvp-next/src/app/gioi-thieu/page.tsx | 1 + .../apps/tpos-mvp-next/src/app/globals.css | 867 ++++++++++++++++++ .../apps/tpos-mvp-next/src/app/login/page.tsx | 4 +- .../apps/tpos-mvp-next/src/app/page.tsx | 171 +--- .../tpos-mvp-next/src/app/project/page.tsx | 1 + .../tpos-mvp-next/src/components/Shell.tsx | 9 +- .../tpos-mvp-next/src/components/TposAuth.tsx | 274 ++++-- .../src/components/TposLoginPortal.tsx | 42 + .../src/components/TposPortal.tsx | 24 +- .../src/components/TposPublicLanding.tsx | 192 ++++ 12 files changed, 1335 insertions(+), 257 deletions(-) create mode 100644 microservices/apps/tpos-mvp-next/src/app/about/page.tsx create mode 100644 microservices/apps/tpos-mvp-next/src/app/gioi-thieu/page.tsx create mode 100644 microservices/apps/tpos-mvp-next/src/app/project/page.tsx create mode 100644 microservices/apps/tpos-mvp-next/src/components/TposLoginPortal.tsx create mode 100644 microservices/apps/tpos-mvp-next/src/components/TposPublicLanding.tsx diff --git a/microservices/apps/tpos-mvp-next/src/app/about/page.tsx b/microservices/apps/tpos-mvp-next/src/app/about/page.tsx new file mode 100644 index 00000000..0039b635 --- /dev/null +++ b/microservices/apps/tpos-mvp-next/src/app/about/page.tsx @@ -0,0 +1,5 @@ +import { TposPublicLanding } from "@/components/TposPublicLanding"; + +export default function AboutPage() { + return ; +} diff --git a/microservices/apps/tpos-mvp-next/src/app/auth/[...path]/page.tsx b/microservices/apps/tpos-mvp-next/src/app/auth/[...path]/page.tsx index 31bda459..f626a789 100644 --- a/microservices/apps/tpos-mvp-next/src/app/auth/[...path]/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/auth/[...path]/page.tsx @@ -1,7 +1,9 @@ import { TposAuthBoundary } from "@/components/TposAuthBoundary"; +import { TposLoginPortal } from "@/components/TposLoginPortal"; export default async function AuthCatchAllPage({ params }: { params: Promise<{ path?: string[] }> }) { const path = (await params).path ?? []; + if (path.length === 0 || (path[0] === "login" && path.length === 1)) return ; const mode = path.includes("register") ? "register" : path.includes("forgot-password-new") || path.includes("password-reset") || path.includes("otp-verify") || path.includes("two-factor") || path.includes("email-sent") ? "recover" : "login"; const role = path[path.length - 1] ?? "admin"; return ; diff --git a/microservices/apps/tpos-mvp-next/src/app/gioi-thieu/page.tsx b/microservices/apps/tpos-mvp-next/src/app/gioi-thieu/page.tsx new file mode 100644 index 00000000..001589ec --- /dev/null +++ b/microservices/apps/tpos-mvp-next/src/app/gioi-thieu/page.tsx @@ -0,0 +1 @@ +export { default } from "../about/page"; diff --git a/microservices/apps/tpos-mvp-next/src/app/globals.css b/microservices/apps/tpos-mvp-next/src/app/globals.css index 9cdf7644..c1c0770b 100644 --- a/microservices/apps/tpos-mvp-next/src/app/globals.css +++ b/microservices/apps/tpos-mvp-next/src/app/globals.css @@ -3615,3 +3615,870 @@ textarea { grid-template-columns: repeat(2, minmax(0, 1fr)); } } + +/* Public landing and login portal parity */ +.public-shell { + min-height: 100vh; + background: + radial-gradient(circle at 50% 0%, rgba(255, 92, 0, 0.14), transparent 34%), + #0a0a0b; + color: #ffffff; + overflow-x: hidden; +} + +.tpos-navbar { + position: sticky; + top: 0; + z-index: 80; + padding: 16px 0; + border-bottom: 1px solid #1f1f23; + background: rgba(10, 10, 11, 0.85); + backdrop-filter: blur(12px); +} + +.tpos-navbar-inner { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; +} + +.tpos-logo { + color: #ff5c00; + font-size: 24px; + font-weight: 800; + text-decoration: none; +} + +.tpos-nav-links { + display: flex; + align-items: center; + gap: 32px; +} + +.tpos-nav-link { + color: #adadb0; + font-size: 14px; + font-weight: 500; + text-decoration: none; +} + +.tpos-nav-link:hover { + color: #ffffff; +} + +.btn-accent { + min-height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 0 24px; + border-radius: 10px; + background: #ff5c00; + color: #ffffff; + font-size: 15px; + font-weight: 600; + text-decoration: none; +} + +.landing-hero { + position: relative; + min-height: 70vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px 24px 48px; + text-align: center; + overflow: hidden; +} + +.landing-hero--project { + min-height: 62vh; +} + +.landing-terminal-preview { + position: absolute; + inset: auto 5% 28px auto; + width: min(560px, 44vw); + min-height: 300px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 24px; + background: rgba(17, 17, 19, 0.5); + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.36); + opacity: 0.42; + transform: rotate(-2deg); + pointer-events: none; +} + +.terminal-bar { + height: 46px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + border-bottom: 1px solid #1f1f23; +} + +.terminal-bar span { + color: #ff5c00; + font-weight: 800; +} + +.terminal-bar b { + color: #22c55e; + font-size: 12px; +} + +.terminal-body { + display: grid; + grid-template-columns: 70px 1fr 160px; + gap: 14px; + padding: 14px; +} + +.terminal-sidebar, +.terminal-order, +.terminal-grid i { + border-radius: 12px; + background: #1a1a1d; +} + +.terminal-sidebar { + min-height: 220px; +} + +.terminal-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.terminal-grid i { + min-height: 104px; + border: 1px solid #2a2a2e; +} + +.terminal-order { + display: grid; + align-content: end; + min-height: 220px; + padding: 14px; + color: #adadb0; +} + +.terminal-order b { + color: #ff5c00; +} + +.home-hero__badge { + position: relative; + z-index: 1; + display: inline-flex; + align-items: center; + gap: 8px; + margin-bottom: 32px; + padding: 8px 20px; + border: 1px solid rgba(255, 92, 0, 0.25); + border-radius: 999px; + background: rgba(255, 92, 0, 0.15); + color: #ff8a4c; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.03em; +} + +.home-hero__title { + position: relative; + z-index: 1; + max-width: 800px; + margin: 0 0 24px; + font-size: 56px; + font-weight: 800; + line-height: 1.08; + letter-spacing: -0.03em; + color: #ffffff; +} + +.home-hero__subtitle { + position: relative; + z-index: 1; + max-width: 640px; + margin: 0 auto 40px; + color: #adadb0; + font-size: 18px; + line-height: 1.7; +} + +.home-hero__actions { + position: relative; + z-index: 1; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 48px; +} + +.home-hero__btn { + min-height: 54px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 0 32px; + border-radius: 14px; + font-size: 16px; + font-weight: 700; + text-decoration: none; +} + +.home-hero__btn--primary { + background: linear-gradient(135deg, #ff5c00 0%, #ff8a4c 100%); + color: #ffffff; + box-shadow: 0 4px 24px rgba(255, 92, 0, 0.3); +} + +.home-hero__btn--secondary { + border: 1px solid #2a2a2e; + background: transparent; + color: #ffffff; +} + +.home-trust { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.home-trust__label { + color: #8b8b90; + font-size: 13px; + text-transform: uppercase; +} + +.home-trust__stats { + display: flex; + gap: 16px; + color: #adadb0; + font-size: 14px; + font-weight: 600; +} + +.home-verticals { + max-width: 900px; + margin: 0 auto; + padding: 48px 24px 64px; +} + +.home-verticals__grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 16px; +} + +.home-vertical-card { + min-height: 128px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + padding: 24px 16px; + border: 1px solid #1f1f23; + border-radius: 14px; + background: #111113; + color: #adadb0; + font-size: 13px; + font-weight: 600; + text-align: center; + text-decoration: none; +} + +.home-vertical-card svg { + color: #ff5c00; +} + +.home-vertical-card:hover { + border-color: #ff5c00; + background: rgba(255, 92, 0, 0.15); + color: #ffffff; +} + +.tpos-section, +.project-intro, +.landing-cta { + max-width: 1200px; + margin: 0 auto; + padding: 72px 24px; +} + +.tpos-section-header { + max-width: 680px; + margin: 0 auto 36px; + text-align: center; +} + +.tpos-section-title, +.project-intro h2, +.landing-cta h2 { + margin: 0 0 14px; + color: #ffffff; + font-size: 36px; + font-weight: 800; + line-height: 1.15; +} + +.tpos-section-desc, +.project-intro p, +.landing-cta p { + color: #adadb0; + font-size: 16px; + line-height: 1.7; +} + +.tpos-feature-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 18px; +} + +.tpos-feature-card, +.tpos-pricing-card { + border: 1px solid #1f1f23; + border-radius: 16px; + background: #111113; + padding: 22px; +} + +.tpos-feature-icon { + width: 44px; + height: 44px; + display: grid; + place-items: center; + margin-bottom: 18px; + border-radius: 12px; + background: rgba(255, 92, 0, 0.15); + color: #ff5c00; +} + +.tpos-feature-card h3, +.tpos-pricing-card h3 { + margin: 0 0 10px; + font-size: 18px; +} + +.tpos-feature-card p, +.tpos-pricing-card p { + margin: 0; + color: #adadb0; + font-size: 14px; + line-height: 1.65; +} + +.project-intro { + display: grid; + grid-template-columns: minmax(0, 1.05fr) 420px; + gap: 36px; + align-items: center; +} + +.project-stack { + display: grid; + gap: 12px; +} + +.project-stack div { + min-height: 54px; + display: flex; + align-items: center; + gap: 12px; + padding: 0 16px; + border: 1px solid #2a2a2e; + border-radius: 12px; + background: #1a1a1d; + color: #ffffff; + font-weight: 600; +} + +.project-stack svg { + color: #22c55e; +} + +.tpos-pricing-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; +} + +.tpos-pricing-card { + position: relative; + display: grid; + gap: 14px; +} + +.tpos-pricing-card.featured { + border-color: #ff5c00; + box-shadow: 0 0 0 1px rgba(255, 92, 0, 0.35); +} + +.tpos-pricing-badge { + width: fit-content; + padding: 4px 10px; + border-radius: 999px; + background: rgba(255, 92, 0, 0.16); + color: #ff8a4c; + font-size: 12px; + font-weight: 700; +} + +.tpos-pricing-card strong { + color: #ffffff; + font-size: 30px; +} + +.tpos-pricing-card a { + display: inline-flex; + align-items: center; + gap: 6px; + color: #ff5c00; + font-weight: 700; + text-decoration: none; +} + +.landing-cta { + display: grid; + justify-items: center; + text-align: center; +} + +.landing-cta svg { + color: #ff5c00; +} + +.landing-cta div { + display: flex; + gap: 12px; + margin-top: 18px; +} + +.login-portal { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80px 24px 60px; + background: #0a0a0b; + color: #ffffff; + font-family: var(--tpos-font); +} + +.login-portal__brand { + position: absolute; + top: 24px; + left: 24px; + color: #ff5c00; + font-size: 24px; + font-weight: 800; + text-decoration: none; +} + +.login-portal__head { + text-align: center; +} + +.login-portal__head h1 { + margin: 0 0 8px; + font-size: 28px; + font-weight: 800; +} + +.login-portal__head p, +.login-portal__footer { + color: #adadb0; + font-size: 15px; +} + +.login-portal__grid { + width: 100%; + max-width: 560px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + margin-top: 40px; +} + +.login-role-card { + min-height: 176px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 28px 20px; + border: 2px solid #2a2a2e; + border-radius: 12px; + background: #1a1a1d; + color: #ffffff; + text-align: center; + text-decoration: none; +} + +.login-role-card:hover { + border-color: var(--role-color); + transform: translateY(-2px); +} + +.login-role-card span { + width: 48px; + height: 48px; + display: grid; + place-items: center; + border-radius: 12px; + background: color-mix(in srgb, var(--role-color) 16%, transparent); + color: var(--role-color); +} + +.login-role-card h2 { + margin: 0; + font-size: 15px; + font-weight: 700; +} + +.login-role-card p { + margin: 0; + color: #adadb0; + font-size: 12px; + line-height: 1.4; +} + +.login-portal__footer { + margin: 32px 0 0; +} + +.login-portal__footer a, +.login-portal__super { + color: #ff5c00; + font-weight: 600; + text-decoration: none; +} + +.login-portal__super { + display: inline-flex; + align-items: center; + gap: 6px; + margin-top: 14px; + font-size: 12px; +} + +@media (max-width: 820px) { + .tpos-nav-links { + gap: 16px; + } + + .tpos-nav-link:nth-child(2), + .tpos-nav-link:nth-child(3), + .tpos-nav-link:nth-child(4) { + display: none; + } + + .landing-terminal-preview { + display: none; + } + + .home-hero__title { + font-size: 40px; + } + + .home-verticals__grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .tpos-feature-grid, + .project-intro, + .tpos-pricing-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 520px) { + .tpos-navbar-inner { + padding: 0 16px; + } + + .btn-accent { + display: none; + } + + .home-hero__title { + font-size: 34px; + } + + .home-hero__subtitle { + font-size: 15px; + } + + .home-hero__actions, + .landing-cta div { + width: 100%; + flex-direction: column; + } + + .home-hero__btn { + width: 100%; + } + + .home-verticals__grid, + .login-portal__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +/* Auth role pages parity */ +.auth-top-nav { + position: sticky; + top: 0; + z-index: 90; + height: 72px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; + border-bottom: 1px solid #1f1f23; + background: rgba(10, 10, 11, 0.88); + backdrop-filter: blur(12px); +} + +.auth-top-nav > div { + display: flex; + align-items: center; + gap: 24px; +} + +.auth-top-nav a:not(.btn-accent) { + color: #adadb0; + font-size: 14px; + font-weight: 500; + text-decoration: none; +} + +.auth-top-nav .tpos-logo { + color: #ff5c00 !important; + font-size: 24px; + font-weight: 800; +} + +.auth-top-nav a:hover { + color: #ffffff; +} + +.auth-screen { + min-height: calc(100vh - 72px); +} + +.auth-screen--split { + grid-template-columns: minmax(420px, 0.95fr) minmax(360px, 440px); + justify-content: stretch; + align-content: stretch; + gap: 0; + padding: 0; +} + +.auth-brand-panel { + min-height: calc(100vh - 72px); + display: flex; + flex-direction: column; + justify-content: center; + padding: 64px; + background: + radial-gradient(circle at 20% 10%, rgba(255, 255, 255, 0.16), transparent 24%), + linear-gradient(180deg, #ff5c00 0%, #ff8a4c 52%, #ffb347 100%); + color: #ffffff; +} + +.auth-screen--customer .auth-brand-panel { + background: + radial-gradient(circle at 80% 20%, rgba(255, 92, 0, 0.16), transparent 30%), + linear-gradient(135deg, #0a0a0b 0%, #1a1a1d 50%, #0a0a0b 100%); +} + +.auth-brand-mark { + width: 72px; + height: 72px; + display: grid; + place-items: center; + margin-bottom: 24px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.18); +} + +.auth-brand-panel h1 { + max-width: 460px; + margin: 0 0 16px; + font-size: 42px; + font-weight: 800; + line-height: 1.08; +} + +.auth-brand-panel p { + max-width: 480px; + color: rgba(255, 255, 255, 0.86); + font-size: 16px; + line-height: 1.7; +} + +.auth-brand-stats { + display: flex; + gap: 12px; + margin-top: 28px; +} + +.auth-brand-stats span { + padding: 8px 12px; + border-radius: 999px; + background: rgba(0, 0, 0, 0.18); + font-size: 12px; + font-weight: 700; +} + +.auth-screen--split .auth-card { + align-self: center; + justify-self: center; +} + +.auth-card__head { + flex-direction: column; + align-items: center; + gap: 10px; + text-align: center; +} + +.auth-card__head p { + margin: 0; + color: #adadb0; + font-size: 14px; + line-height: 1.5; +} + +.auth-role-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 14px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.03em; +} + +.auth-role-badge--blue, +.auth-card--blue .auth-submit { + --auth-tone: #3b82f6; +} + +.auth-role-badge--green, +.auth-card--green .auth-submit { + --auth-tone: #22c55e; +} + +.auth-role-badge--pink, +.auth-card--pink .auth-submit { + --auth-tone: #ec4899; +} + +.auth-role-badge--orange, +.auth-card--orange .auth-submit { + --auth-tone: #ff5c00; +} + +.auth-role-badge { + border: 1px solid color-mix(in srgb, var(--auth-tone) 28%, transparent); + background: color-mix(in srgb, var(--auth-tone) 15%, transparent); + color: var(--auth-tone); +} + +.auth-card--blue .auth-submit, +.auth-card--green .auth-submit, +.auth-card--pink .auth-submit, +.auth-card--orange .auth-submit { + background: var(--auth-tone); +} + +.auth-security-hint { + width: 100%; + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + padding: 10px 16px; + border-radius: 10px; + color: var(--auth-tone, #ff5c00); + background: color-mix(in srgb, var(--auth-tone, #ff5c00) 9%, transparent); + border: 1px solid color-mix(in srgb, var(--auth-tone, #ff5c00) 20%, transparent); + font-size: 13px; +} + +.auth-alt-links { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 14px; + margin-top: 16px; +} + +.auth-alt-links a, +.auth-inline-link { + color: #ff5c00; + font-size: 13px; + font-weight: 600; + text-decoration: none; +} + +.auth-social-row { + display: grid; + gap: 10px; + margin: 12px 0; + color: #8b8b90; + font-size: 12px; + text-align: center; +} + +.auth-social-row div { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.auth-social-row button { + min-height: 38px; + border: 1px solid #2a2a2e; + border-radius: 10px; + background: #1a1a1d; + color: #ffffff; + font-weight: 600; +} + +@media (max-width: 820px) { + .auth-top-nav > div a:not(.btn-accent) { + display: none; + } + + .auth-screen--split { + grid-template-columns: 1fr; + } + + .auth-brand-panel { + display: none; + } + + .auth-screen--split .auth-card { + margin: 24px; + } +} diff --git a/microservices/apps/tpos-mvp-next/src/app/login/page.tsx b/microservices/apps/tpos-mvp-next/src/app/login/page.tsx index a525ed80..d018fffe 100644 --- a/microservices/apps/tpos-mvp-next/src/app/login/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/login/page.tsx @@ -1,5 +1,5 @@ -import { TposAuthBoundary } from "@/components/TposAuthBoundary"; +import { TposLoginPortal } from "@/components/TposLoginPortal"; export default function LoginAliasPage() { - return ; + return ; } diff --git a/microservices/apps/tpos-mvp-next/src/app/page.tsx b/microservices/apps/tpos-mvp-next/src/app/page.tsx index b2d221dc..dc25056a 100644 --- a/microservices/apps/tpos-mvp-next/src/app/page.tsx +++ b/microservices/apps/tpos-mvp-next/src/app/page.tsx @@ -1,170 +1,5 @@ -import Link from "next/link"; -import { Activity, AlertTriangle, ShoppingBag, Store } from "lucide-react"; -import { createShopAction } from "@/app/actions"; -import { AppFrame } from "@/components/AppFrame"; -import { EmptyState, Metric, PageHeader, StatusPill, money } from "@/components/Primitives"; -import { getDashboardStats } from "@/server/db/queries"; -import { getShopService } from "@/server/services/shop"; -import { listActivity } from "@/server/db/queries"; -import { serviceMap, verticalOptions } from "@/server/domain/catalog"; +import { TposPublicLanding } from "@/components/TposPublicLanding"; -export const dynamic = "force-dynamic"; - -type PageProps = { - searchParams?: Promise<{ shopId?: string }>; -}; - -export default async function DashboardPage({ searchParams }: PageProps) { - const params = (await searchParams) ?? {}; - const shop = await getShopService(params.shopId); - const stats = await getDashboardStats(shop?.id ?? null); - const activity = await listActivity(); - - return ( - - - {shop ? ( - - - Mở POS - - ) : null} - - - {!shop ? ( -
-
-

Thiết lập cửa hàng

- - - -
- Dashboard sẽ xuất hiện sau khi tạo cửa hàng đầu tiên. -
- ) : ( - <> -
- - - - 0 ? "warn" : "neutral"} /> -
- -
-
-
-

Đơn gần đây

- Xem tất cả -
- {stats.recentOrders.length ? ( - - - - - - - - - - - {stats.recentOrders.map((order) => ( - - - - - - - ))} - -
Mã đơnMónTrạng tháiTổng
{order.transactionId ?? order.id.slice(0, 8)}{order.itemCount} - - {money(order.totalAmount)}
- ) : ( - - )} -
- -
-
-

Service map MVP

- -
- {serviceMap.map((service) => { - const Icon = service.icon; - return ( -
- - - {service.name} - - {service.label} -
- ); - })} -
- -
-
-

Tồn kho cảnh báo

- -
- {stats.lowStock.length ? ( - - - - - - - - - - {stats.lowStock.map((item) => ( - - - - - - ))} - -
Mặt hàngTồnMức đặt lại
{item.name ?? item.productName}{item.quantity}{item.reorderLevel}
- ) : ( - - )} -
- -
-
-

Hoạt động

- -
-
- {activity.map((item) => ( -
- {item.action} - {new Date(item.createdAt).toLocaleString("vi-VN")} -
- ))} - {!activity.length ? : null} -
-
-
- - )} -
- ); +export default function LandingPage() { + return ; } diff --git a/microservices/apps/tpos-mvp-next/src/app/project/page.tsx b/microservices/apps/tpos-mvp-next/src/app/project/page.tsx new file mode 100644 index 00000000..001589ec --- /dev/null +++ b/microservices/apps/tpos-mvp-next/src/app/project/page.tsx @@ -0,0 +1 @@ +export { default } from "../about/page"; diff --git a/microservices/apps/tpos-mvp-next/src/components/Shell.tsx b/microservices/apps/tpos-mvp-next/src/components/Shell.tsx index b76ad041..7931b632 100644 --- a/microservices/apps/tpos-mvp-next/src/components/Shell.tsx +++ b/microservices/apps/tpos-mvp-next/src/components/Shell.tsx @@ -17,7 +17,7 @@ import type { ReactNode } from "react"; import type { Shop } from "@/server/domain/types"; const nav = [ - { href: "/", label: "Tổng quan", icon: Gauge }, + { href: "/admin", label: "Quản trị", icon: Gauge }, { href: "/pos", label: "Bán hàng", icon: TerminalSquare }, { href: "/catalog", label: "Sản phẩm", icon: PackagePlus }, { href: "/inventory", label: "Kho", icon: Boxes }, @@ -39,6 +39,7 @@ export function Shell({ const router = useRouter(); const params = useSearchParams(); const shopQuery = currentShop ? `?shopId=${currentShop.id}` : ""; + const adminHref = currentShop ? `/admin/shop/${currentShop.id}/overview` : "/admin"; function switchShop(shopId: string) { const next = new URLSearchParams(params.toString()); @@ -49,7 +50,7 @@ export function Shell({ return (
- {(payload.secondary ?? defaultSecondary(kind)).map((item) => ( + {(payload.secondary ?? defaultSecondary(kind, payload.shop)).map((item) => ( ))}
@@ -190,12 +190,15 @@ function ShopSectionPanel({ shop }: { shop: NonNullable }
- {sections.map(([label, slug, Icon]) => ( - - - {label} - - ))} + {sections.map(([label, slug, Icon]) => { + const href = slug === "pos" ? `/pos/${shop.id}/${vertical}` : `/admin/shop/${shop.id}/${slug}`; + return ( + + + {label} + + ); + })}
); @@ -237,15 +240,16 @@ function defaultItems(kind: PortalKind): ListItem[] { ]; } -function defaultSecondary(kind: PortalKind): ListItem[] { +function defaultSecondary(kind: PortalKind, shop?: PortalPayload["shop"]): ListItem[] { if (kind === "superadmin") return [ { title: "System health", href: "/superadmin/system/health" }, { title: "Feature flags", href: "/superadmin/system/flags" }, { title: "Audit log", href: "/superadmin/system/audit" } ]; + const vertical = shop?.vertical ?? "cafe"; return [ - { title: "Customer menu", href: "/" }, - { title: "POS Café", href: "/pos" }, + { title: "Customer menu", href: shop ? `/menu/${shop.id}` : "/" }, + { title: "POS terminal", href: shop ? `/pos/${shop.id}/${vertical}` : "/pos" }, { title: "Settings", href: "/settings" } ]; } diff --git a/microservices/apps/tpos-mvp-next/src/components/TposPublicLanding.tsx b/microservices/apps/tpos-mvp-next/src/components/TposPublicLanding.tsx new file mode 100644 index 00000000..87792e4c --- /dev/null +++ b/microservices/apps/tpos-mvp-next/src/components/TposPublicLanding.tsx @@ -0,0 +1,192 @@ +import Link from "next/link"; +import { + ArrowRight, + BarChart3, + Bot, + Check, + Coffee, + Heart, + LogIn, + Mic, + ShieldCheck, + ShoppingBag, + Sparkles, + Store, + UtensilsCrossed, + Zap +} from "lucide-react"; + +const verticals = [ + { label: "Cafe", icon: Coffee }, + { label: "Nhà hàng & F&B", icon: UtensilsCrossed }, + { label: "Karaoke", icon: Mic }, + { label: "TMV/Spa", icon: Sparkles }, + { label: "Bán lẻ", icon: ShoppingBag } +]; + +const features = [ + { title: "POS đa ngành", desc: "Cafe, nhà hàng, karaoke, spa, beauty và retail cùng một lõi vận hành.", icon: Store }, + { title: "AI vận hành", desc: "Chat trợ lý, gợi ý tồn kho, phân tích doanh thu và workflow marketing.", icon: Bot }, + { title: "Loyalty & CRM", desc: "Tích điểm, ví thành viên, voucher, chiến dịch và chăm sóc khách hàng.", icon: Heart }, + { title: "Báo cáo realtime", desc: "Doanh thu, payment ledger, EOD, top products và sức khỏe nền tảng.", icon: BarChart3 } +]; + +const plans = [ + { name: "Starter", price: "Miễn phí", desc: "Một cửa hàng, POS cơ bản, menu QR.", featured: false }, + { name: "Growth", price: "299K", desc: "Đa ngành, inventory, staff, loyalty.", featured: true }, + { name: "Scale", price: "Liên hệ", desc: "Nhiều chi nhánh, marketing, AI và API.", featured: false } +]; + +export function TposPublicLanding({ variant = "home" }: { variant?: "home" | "project" }) { + const isProject = variant === "project"; + + return ( +
+ + +
+
+ +
+
+ {verticals.map(({ label, icon: Icon }) => ( + + + {label} + + ))} +
+
+ +
+
+ TPOS Modules +

Một nền tảng cho toàn bộ vòng đời bán hàng

+

Từ QR menu, POS terminal, bếp/barista, inventory, staff đến marketing CRM và superadmin.

+
+
+ {features.map(({ title, desc, icon: Icon }) => ( +
+ +

{title}

+

{desc}

+
+ ))} +
+
+ +
+
+ Project Architecture +

Next MVP chạy độc lập, lấy web-client-tpos-net làm chuẩn giao diện

+

+ MVP dùng Next App Router, BFF route surface, httpOnly session và PostgreSQL schema nội bộ. + Các microservice .NET hiện có là reference contract, không phải dependency runtime. +

+
+
+ {["Landing & Auth", "Role Portals", "POS Verticals", "BFF + MVP DB"].map((item) => ( +
+ + {item} +
+ ))} +
+
+ +
+
+ Plans +

Gói triển khai theo quy mô vận hành

+
+
+ {plans.map((plan) => ( +
+ {plan.featured ? Khuyến nghị : null} +

{plan.name}

+ {plan.price} +

{plan.desc}

+ Bắt đầu +
+ ))} +
+
+ +
+ +

Sẵn sàng mở portal vận hành?

+

Chọn vai trò đăng nhập hoặc tạo merchant mới để tiếp tục setup cửa hàng MVP.

+
+ Portal đăng nhập + Vào admin demo +
+
+
+ ); +} + +function PublicNav() { + return ( + + ); +}