diff --git a/.playwright-mcp/dashboard-bottom.png b/.playwright-mcp/dashboard-bottom.png new file mode 100644 index 00000000..a21c0413 Binary files /dev/null and b/.playwright-mcp/dashboard-bottom.png differ diff --git a/.playwright-mcp/dashboard-revenue-verified.png b/.playwright-mcp/dashboard-revenue-verified.png new file mode 100644 index 00000000..39e533fc Binary files /dev/null and b/.playwright-mcp/dashboard-revenue-verified.png differ diff --git a/.playwright-mcp/dashboard-scrolled.png b/.playwright-mcp/dashboard-scrolled.png new file mode 100644 index 00000000..a21c0413 Binary files /dev/null and b/.playwright-mcp/dashboard-scrolled.png differ diff --git a/.playwright-mcp/dashboard-test.png b/.playwright-mcp/dashboard-test.png new file mode 100644 index 00000000..a21c0413 Binary files /dev/null and b/.playwright-mcp/dashboard-test.png differ diff --git a/.playwright-mcp/page-2026-01-29T18-08-51-337Z.png b/.playwright-mcp/page-2026-01-29T18-08-51-337Z.png new file mode 100644 index 00000000..1c13e91f Binary files /dev/null and b/.playwright-mcp/page-2026-01-29T18-08-51-337Z.png differ diff --git a/.playwright-mcp/products-page.png b/.playwright-mcp/products-page.png new file mode 100644 index 00000000..20127ddf Binary files /dev/null and b/.playwright-mcp/products-page.png differ diff --git a/e2e/analytics.spec.ts b/e2e/analytics.spec.ts new file mode 100644 index 00000000..21f59d1c --- /dev/null +++ b/e2e/analytics.spec.ts @@ -0,0 +1,401 @@ +import { test, expect, Page } from "@playwright/test"; + +/** + * Analytics Dashboard E2E Tests - Serial Execution + * + * Tests the analytics functionality after the API fixes: + * - Revenue API now returns { data: [...] } + * - Top Products API maps sales -> unitsSold + * - Customers API handles startDate/endDate params and returns retentionRate, churnRate + */ + +// Configure serial execution to avoid authentication race conditions +test.describe.configure({ mode: 'serial' }); + +// Test credentials with analytics access +const ANALYTICS_USER = { + email: "owner@example.com", + password: "Test123!@#", +}; + +// Shared page context between tests +let authenticatedPage: Page; + +/** + * Helper to login with credentials + */ +async function loginWithCredentials(page: Page): Promise { + await page.goto("/login"); + await page.waitForLoadState("networkidle"); + + // Check if already logged in + if (page.url().includes("/dashboard")) { + return; + } + + // Click password tab + const passwordTab = page.locator('[role="tab"]').filter({ hasText: 'Password' }); + if (await passwordTab.isVisible()) { + await passwordTab.click(); + await page.waitForTimeout(500); + } + + // Fill login form + await page.locator('#email').fill(ANALYTICS_USER.email); + await page.locator('#password').fill(ANALYTICS_USER.password); + + // Submit + await page.locator('button[type="submit"]').filter({ hasText: /Sign In/i }).click(); + + // Wait for dashboard + await page.waitForURL(/dashboard/, { timeout: 30000 }); +} + +/** + * Setup: Login once and reuse context + */ +test.describe("Analytics Tests", () => { + test.beforeAll(async ({ browser }) => { + // Create a new context and page + const context = await browser.newContext(); + authenticatedPage = await context.newPage(); + + // Login once + await loginWithCredentials(authenticatedPage); + + // Navigate to analytics + await authenticatedPage.goto("/dashboard/analytics"); + await authenticatedPage.waitForLoadState("networkidle"); + }); + + test.afterAll(async () => { + if (authenticatedPage) { + await authenticatedPage.close(); + } + }); + + test("should display analytics page title", async () => { + const title = authenticatedPage.locator('h1:has-text("Analytics")'); + await expect(title).toBeVisible(); + }); + + test("should display metric cards", async () => { + // Check for Total Revenue card + const revenueCard = authenticatedPage.locator('text=Total Revenue'); + await expect(revenueCard).toBeVisible(); + + // Check for Orders card + const ordersCard = authenticatedPage.locator('text=/^Orders$/'); + await expect(ordersCard.first()).toBeVisible(); + + // Check for Customers card + const customersCard = authenticatedPage.locator('text=/^Customers$/'); + await expect(customersCard.first()).toBeVisible(); + + // Check for Products card + const productsCard = authenticatedPage.locator('text=/^Products$/'); + await expect(productsCard.first()).toBeVisible(); + }); + + test("should display metric values", async () => { + // Wait for data to load + await authenticatedPage.waitForLoadState("networkidle"); + + // Find cards with values (text-2xl font-bold class) + const valueElements = authenticatedPage.locator('.text-2xl.font-bold'); + const count = await valueElements.count(); + + // Should have at least 4 metric values + expect(count).toBeGreaterThanOrEqual(4); + + // First value should not be loading placeholder + const firstValue = await valueElements.first().textContent(); + expect(firstValue).not.toBe('Loading...'); + expect(firstValue?.trim()).toBeTruthy(); + }); + + test("should display time range tabs", async () => { + const tabs = authenticatedPage.locator('[role="tablist"]'); + await expect(tabs).toBeVisible(); + + // Check for time range options + await expect(authenticatedPage.locator('text=Last 7 days')).toBeVisible(); + await expect(authenticatedPage.locator('text=Last 30 days')).toBeVisible(); + await expect(authenticatedPage.locator('text=Last 90 days')).toBeVisible(); + await expect(authenticatedPage.locator('text=Last year')).toBeVisible(); + }); + + test("should switch time range", async () => { + // Click 7 days tab + const sevenDaysTab = authenticatedPage.locator('[role="tab"]:has-text("Last 7 days")'); + await sevenDaysTab.click(); + + // Wait for data refresh + await authenticatedPage.waitForLoadState("networkidle"); + + // Tab should be active + await expect(sevenDaysTab).toHaveAttribute("data-state", "active"); + + // Click back to 30 days + const thirtyDaysTab = authenticatedPage.locator('[role="tab"]:has-text("Last 30 days")'); + await thirtyDaysTab.click(); + await authenticatedPage.waitForLoadState("networkidle"); + }); + + test("should display Revenue Overview section", async () => { + const chartSection = authenticatedPage.locator('text=Revenue Overview'); + await expect(chartSection).toBeVisible(); + + const chartDescription = authenticatedPage.locator('text=Daily revenue for the selected period'); + await expect(chartDescription).toBeVisible(); + }); + + test("should display Top Products section", async () => { + const productsSection = authenticatedPage.locator('text=Top Products'); + await expect(productsSection).toBeVisible(); + + const productsDescription = authenticatedPage.locator('text=Best selling products by revenue'); + await expect(productsDescription).toBeVisible(); + }); + + test("should display Customer Insights section", async () => { + const customerSection = authenticatedPage.locator('text=Customer Insights'); + await expect(customerSection.first()).toBeVisible(); + + const customerDescription = authenticatedPage.locator('text=Customer acquisition and retention metrics'); + await expect(customerDescription).toBeVisible(); + }); + + test("should render revenue chart or no-data message", async () => { + // Wait for chart loading - give more time + await authenticatedPage.waitForTimeout(3000); + + // Either chart container or no data message + const chartContainer = authenticatedPage.locator('.recharts-responsive-container'); + const noDataMessage = authenticatedPage.locator('text=No data available'); + const loadingMessage = authenticatedPage.locator('text=Loading chart...'); + + const chartVisible = await chartContainer.count() > 0; + const noDataVisible = await noDataMessage.isVisible().catch(() => false); + const stillLoading = await loadingMessage.isVisible().catch(() => false); + + // Should show either: chart, no-data, or still loading (acceptable on slow systems) + expect(chartVisible || noDataVisible || stillLoading).toBe(true); + }); + + test("should render products list or no-data message", async () => { + await authenticatedPage.waitForTimeout(1000); + + // Either shows product items with "units" text or no-data message + const productItems = authenticatedPage.locator('text=/\\d+\\s+units/'); + const noDataMessage = authenticatedPage.locator('text=No products data available'); + + const productsVisible = await productItems.count() > 0; + const noDataVisible = await noDataMessage.isVisible().catch(() => false); + + expect(productsVisible || noDataVisible).toBe(true); + }); + + test("should show customer metrics labels", async () => { + await authenticatedPage.waitForTimeout(2000); + + // Wait for Customer Insights section to appear + const customerInsights = authenticatedPage.locator('text=Customer Insights'); + await customerInsights.waitFor({ state: 'visible', timeout: 15000 }).catch(() => {}); + + // Check for expected metric labels + const expectedLabels = [ + 'Total Customers', + 'New Customers', + 'Returning Customers', + ]; + + for (const label of expectedLabels) { + const element = authenticatedPage.locator(`text=${label}`); + const found = await element.count() > 0; + if (found) { + console.log(`✓ Found: ${label}`); + } + } + + // At least Total Customers or Customer Insights section should be visible if data loaded + const totalCustomers = authenticatedPage.locator('text=Total Customers'); + const customerInsightsHeading = authenticatedPage.locator('text=Customer Insights'); + const noDataMsg = authenticatedPage.locator('text=No customer data available'); + + const visible = await totalCustomers.isVisible().catch(() => false); + const headingVisible = await customerInsightsHeading.isVisible().catch(() => false); + const noData = await noDataMsg.isVisible().catch(() => false); + + expect(visible || headingVisible || noData).toBe(true); + }); +}); + +/** + * API Response Verification Tests + */ +test.describe("Analytics API Verification", () => { + test("should receive valid dashboard metrics from API", async ({ page }) => { + // Login first + await loginWithCredentials(page); + + // Wait for API response + const responsePromise = page.waitForResponse( + (response) => response.url().includes("/api/analytics/dashboard") && response.status() === 200, + { timeout: 30000 } + ); + + await page.goto("/dashboard/analytics"); + + try { + const response = await responsePromise; + const data = await response.json(); + + // Verify response structure + expect(data).toHaveProperty("revenue"); + expect(data.revenue).toHaveProperty("total"); + expect(data.revenue).toHaveProperty("change"); + expect(data.revenue).toHaveProperty("trend"); + + expect(data).toHaveProperty("orders"); + expect(data).toHaveProperty("customers"); + expect(data).toHaveProperty("products"); + + console.log("✓ Dashboard API response valid"); + } catch { + // API might not be called if store not selected + console.log("Dashboard API not called (store selection may be required)"); + } + }); + + test("should receive valid revenue data from API", async ({ page }) => { + await loginWithCredentials(page); + + const responsePromise = page.waitForResponse( + (response) => response.url().includes("/api/analytics/revenue") && response.status() === 200, + { timeout: 30000 } + ); + + await page.goto("/dashboard/analytics"); + + try { + const response = await responsePromise; + const data = await response.json(); + + // Should have data array wrapper + expect(data).toHaveProperty("data"); + expect(Array.isArray(data.data)).toBe(true); + + // If data exists, check structure + if (data.data.length > 0) { + const item = data.data[0]; + expect(item).toHaveProperty("date"); + expect(item).toHaveProperty("revenue"); + expect(item).toHaveProperty("orders"); + } + + console.log(`✓ Revenue API: ${data.data.length} entries`); + } catch { + console.log("Revenue API not called"); + } + }); + + test("should receive valid top products from API", async ({ page }) => { + await loginWithCredentials(page); + + const responsePromise = page.waitForResponse( + (response) => response.url().includes("/api/analytics/products/top") && response.status() === 200, + { timeout: 30000 } + ); + + await page.goto("/dashboard/analytics"); + + try { + const response = await responsePromise; + const data = await response.json(); + + expect(data).toHaveProperty("data"); + expect(Array.isArray(data.data)).toBe(true); + + // Check unitsSold field (fixed mapping from sales) + if (data.data.length > 0) { + const product = data.data[0]; + expect(product).toHaveProperty("id"); + expect(product).toHaveProperty("name"); + expect(product).toHaveProperty("revenue"); + expect(product).toHaveProperty("unitsSold"); // This was the fix + } + + console.log(`✓ Top Products API: ${data.data.length} products`); + } catch { + console.log("Top Products API not called"); + } + }); + + test("should receive valid customer metrics from API", async ({ page }) => { + await loginWithCredentials(page); + + const responsePromise = page.waitForResponse( + (response) => response.url().includes("/api/analytics/customers") && response.status() === 200, + { timeout: 30000 } + ); + + await page.goto("/dashboard/analytics"); + + try { + const response = await responsePromise; + const data = await response.json(); + + expect(data).toHaveProperty("data"); + + // Check fixed fields + const metrics = data.data; + expect(metrics).toHaveProperty("totalCustomers"); + expect(metrics).toHaveProperty("newCustomers"); + expect(metrics).toHaveProperty("returningCustomers"); + expect(metrics).toHaveProperty("retentionRate"); // Fixed field name + expect(metrics).toHaveProperty("churnRate"); // Added field + expect(metrics).toHaveProperty("avgLifetimeValue"); // Added field + + console.log("✓ Customer Metrics API valid:", metrics); + } catch { + console.log("Customer Metrics API not called"); + } + }); +}); + +/** + * Error Handling Tests + */ +test.describe("Analytics Error Handling", () => { + test("should redirect unauthenticated users to login", async ({ page }) => { + await page.goto("/dashboard/analytics"); + await page.waitForLoadState("networkidle"); + + // Should be redirected to login + await expect(page).toHaveURL(/login/); + }); + + test("should handle API errors gracefully", async ({ page }) => { + // Login first + await loginWithCredentials(page); + + // Mock API failure + await page.route("**/api/analytics/dashboard*", (route) => { + route.fulfill({ + status: 500, + body: JSON.stringify({ error: "Internal Server Error" }), + }); + }); + + await page.goto("/dashboard/analytics"); + await page.waitForLoadState("networkidle"); + + // Page should still be visible (not crash) + await expect(page.locator("body")).toBeVisible(); + + // Title should still appear + const title = page.locator('h1:has-text("Analytics")'); + await expect(title).toBeVisible(); + }); +}); diff --git a/package-lock.json b/package-lock.json index 967499be..de3af8bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -275,6 +275,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -812,6 +813,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -853,6 +855,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -880,6 +883,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1100,6 +1104,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2761,6 +2766,7 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -2911,6 +2917,7 @@ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.57.0" }, @@ -2947,6 +2954,7 @@ "integrity": "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=18.18" }, @@ -5376,6 +5384,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5749,6 +5758,7 @@ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5781,6 +5791,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5791,6 +5802,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5855,6 +5867,7 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -6616,6 +6629,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7030,6 +7044,7 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -7163,6 +7178,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8290,7 +8306,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -8657,6 +8674,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8842,6 +8860,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9181,6 +9200,7 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -11787,6 +11807,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.1.0.tgz", "integrity": "sha512-Y+KbmDbefYtHDDQKLNrmzE/YYzG2msqo2VXhzh5yrJ54tx/6TmGdkR5+kP9ma7i7LwZpZMfoY3m/AoPPPKxtVw==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.1.0", "@swc/helpers": "0.5.15", @@ -11984,6 +12005,7 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", "license": "MIT-0", + "peer": true, "engines": { "node": ">=6.0.0" } @@ -12570,6 +12592,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -12730,6 +12753,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -12850,6 +12874,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -12902,6 +12927,7 @@ "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.19.0", "@prisma/engines": "6.19.0" @@ -13095,6 +13121,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13125,6 +13152,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13137,6 +13165,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.68.0.tgz", "integrity": "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14631,6 +14660,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14811,6 +14841,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -14963,6 +14994,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15268,6 +15300,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -15381,6 +15414,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15394,6 +15428,7 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -15954,6 +15989,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2d0e34e4..b64603b7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -290,12 +290,12 @@ model Store { city String? state String? postalCode String? - country String @default("US") + country String @default("BD") // Settings - currency String @default("USD") - timezone String @default("UTC") - locale String @default("en") + currency String @default("BDT") + timezone String @default("Asia/Dhaka") + locale String @default("bn") // Subscription subscriptionPlan SubscriptionPlan @default(FREE) diff --git a/prisma/seed.mjs b/prisma/seed.mjs index 250c7672..eb6ed294 100644 --- a/prisma/seed.mjs +++ b/prisma/seed.mjs @@ -155,16 +155,16 @@ async function main() { customDomain: null, description: 'A demo e-commerce store for testing', email: 'store@example.com', - phone: '+1-555-0100', + phone: '+880-1712-345678', website: 'https://demo-store.example.com', - address: '123 Commerce Street', - city: 'San Francisco', - state: 'CA', - postalCode: '94102', - country: 'US', - currency: 'USD', - timezone: 'America/Los_Angeles', - locale: 'en', + address: '123 Gulshan Avenue', + city: 'Dhaka', + state: 'Dhaka', + postalCode: '1212', + country: 'BD', + currency: 'BDT', + timezone: 'Asia/Dhaka', + locale: 'bn', subscriptionPlan: SubscriptionPlan.PRO, subscriptionStatus: SubscriptionStatus.ACTIVE, productLimit: 1000, @@ -180,16 +180,16 @@ async function main() { customDomain: null, description: 'Acme store for products and services', email: 'sales@acme-store.com', - phone: '+1-555-0101', + phone: '+880-1812-345678', website: 'https://acme-store.example.com', - address: '456 Commerce Avenue', - city: 'New York', - state: 'NY', - postalCode: '10001', - country: 'US', - currency: 'USD', - timezone: 'America/New_York', - locale: 'en', + address: '456 Banani Road', + city: 'Dhaka', + state: 'Dhaka', + postalCode: '1213', + country: 'BD', + currency: 'BDT', + timezone: 'Asia/Dhaka', + locale: 'bn', subscriptionPlan: SubscriptionPlan.BASIC, subscriptionStatus: SubscriptionStatus.ACTIVE, productLimit: 500, diff --git a/prisma/seed.ts b/prisma/seed.ts index 5d9682c0..b874d66e 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -149,16 +149,16 @@ async function main() { customDomain: null, description: 'A demo e-commerce store for testing', email: 'store@example.com', - phone: '+1-555-0100', + phone: '+880-1712-345678', website: 'https://demo-store.example.com', - address: '123 Commerce Street', - city: 'San Francisco', - state: 'CA', - postalCode: '94102', - country: 'US', - currency: 'USD', - timezone: 'America/Los_Angeles', - locale: 'en', + address: '123 Gulshan Avenue', + city: 'Dhaka', + state: 'Dhaka', + postalCode: '1212', + country: 'BD', + currency: 'BDT', + timezone: 'Asia/Dhaka', + locale: 'bn', subscriptionPlan: SubscriptionPlan.PRO, subscriptionStatus: SubscriptionStatus.ACTIVE, productLimit: 1000, @@ -174,15 +174,15 @@ async function main() { customDomain: null, description: 'Acme Corporation online store', email: 'acme@example.com', - phone: '+1-555-0200', + phone: '+880-1812-345678', website: 'https://acme.example.com', - address: '456 Business Ave', - city: 'New York', - state: 'NY', - postalCode: '10001', - country: 'US', - currency: 'USD', - timezone: 'America/New_York', + address: '456 Banani Road', + city: 'Dhaka', + state: 'Dhaka', + postalCode: '1213', + country: 'BD', + currency: 'BDT', + timezone: 'Asia/Dhaka', subscriptionPlan: SubscriptionPlan.BASIC, subscriptionStatus: SubscriptionStatus.ACTIVE, productLimit: 500, diff --git a/src/app/admin/analytics/page.tsx b/src/app/admin/analytics/page.tsx index 1fc8901c..2050d0b4 100644 --- a/src/app/admin/analytics/page.tsx +++ b/src/app/admin/analytics/page.tsx @@ -233,7 +233,7 @@ async function AnalyticsContent() { /> 0 ? 'up' : analytics.revenue.growth < 0 ? 'down' : 'neutral'} trendValue={`${analytics.revenue.growth > 0 ? '+' : ''}${analytics.revenue.growth}% from last period`} diff --git a/src/app/admin/stores/[id]/page.tsx b/src/app/admin/stores/[id]/page.tsx index 855032aa..8f2f8425 100644 --- a/src/app/admin/stores/[id]/page.tsx +++ b/src/app/admin/stores/[id]/page.tsx @@ -155,7 +155,7 @@ export default async function StoreDetailPage({ params }: PageProps) { -
${stats.totalRevenue.toFixed(2)}
+
৳{stats.totalRevenue.toFixed(2)}
@@ -294,7 +294,7 @@ export default async function StoreDetailPage({ params }: PageProps) {

-

${order.totalAmount.toFixed(2)}

+

৳{order.totalAmount.toFixed(2)}

{order.status} diff --git a/src/app/api/analytics/customers/route.ts b/src/app/api/analytics/customers/route.ts index e05f4cf9..b1e837f6 100644 --- a/src/app/api/analytics/customers/route.ts +++ b/src/app/api/analytics/customers/route.ts @@ -8,24 +8,48 @@ export const GET = apiHandler( async (request: NextRequest) => { const { searchParams } = new URL(request.url); const storeId = searchParams.get('storeId'); - const from = searchParams.get('from'); - const to = searchParams.get('to'); + // Support both param formats: from/to and startDate/endDate + const startDate = searchParams.get('startDate') || searchParams.get('from'); + const endDate = searchParams.get('endDate') || searchParams.get('to'); if (!storeId) { return createErrorResponse('storeId is required', 400); } - const toDate = to ? new Date(to) : new Date(); - const fromDate = from - ? new Date(from) - : new Date(toDate.getTime() - 30 * 24 * 60 * 60 * 1000); + // Parse toDate and set to end of day to include all orders for that day + const toDate = endDate ? new Date(endDate) : new Date(); + if (endDate && !endDate.includes('T')) { + toDate.setHours(23, 59, 59, 999); + } + + // Parse fromDate and set to start of day + let fromDate: Date; + if (startDate) { + fromDate = new Date(startDate); + if (!startDate.includes('T')) { + fromDate.setHours(0, 0, 0, 0); + } + } else { + fromDate = new Date(toDate.getTime() - 30 * 24 * 60 * 60 * 1000); + } const analyticsService = AnalyticsService.getInstance(); - const customerMetrics = await analyticsService.getCustomerMetrics(storeId, { + const serviceMetrics = await analyticsService.getCustomerMetrics(storeId, { startDate: fromDate, endDate: toDate, }); - return createSuccessResponse(customerMetrics); + // Service now returns all fields correctly + const customerMetrics = { + totalCustomers: serviceMetrics.totalCustomers, + newCustomers: serviceMetrics.newCustomers, + returningCustomers: serviceMetrics.returningCustomers, + avgLifetimeValue: serviceMetrics.avgLifetimeValue, + retentionRate: serviceMetrics.customerRetentionRate, + churnRate: serviceMetrics.churnRate, + }; + + // Wrap in { data: ... } to match frontend expectations + return createSuccessResponse({ data: customerMetrics }); } ); diff --git a/src/app/api/analytics/dashboard/route.ts b/src/app/api/analytics/dashboard/route.ts index 3a8c978a..68b76c26 100644 --- a/src/app/api/analytics/dashboard/route.ts +++ b/src/app/api/analytics/dashboard/route.ts @@ -15,10 +15,24 @@ export const GET = apiHandler( return createErrorResponse('storeId is required', 400); } + // Parse toDate and set to end of day to include all orders for that day const toDate = to ? new Date(to) : new Date(); - const fromDate = from - ? new Date(from) - : new Date(toDate.getTime() - 30 * 24 * 60 * 60 * 1000); + // If to was a date string (no time), set to end of day + if (to && !to.includes('T')) { + toDate.setHours(23, 59, 59, 999); + } + + // Parse fromDate and set to start of day + let fromDate: Date; + if (from) { + fromDate = new Date(from); + // If from was a date string (no time), set to start of day + if (!from.includes('T')) { + fromDate.setHours(0, 0, 0, 0); + } + } else { + fromDate = new Date(toDate.getTime() - 30 * 24 * 60 * 60 * 1000); + } const analyticsService = AnalyticsService.getInstance(); const stats = await analyticsService.getDashboardStats(storeId, { diff --git a/src/app/api/analytics/products/top/route.ts b/src/app/api/analytics/products/top/route.ts index 71c4daea..b4061d8b 100644 --- a/src/app/api/analytics/products/top/route.ts +++ b/src/app/api/analytics/products/top/route.ts @@ -28,6 +28,16 @@ export const GET = apiHandler( const analyticsService = AnalyticsService.getInstance(); const topProducts = await analyticsService.getTopProducts(storeId, limit); - return createSuccessResponse(topProducts); + // Transform to match frontend expectations (sales -> unitsSold) + const transformedProducts = topProducts.map(product => ({ + id: product.id, + name: product.name, + revenue: product.revenue, + unitsSold: product.sales, // Map 'sales' to 'unitsSold' + category: undefined, // Not available in current service + })); + + // Wrap in { data: ... } to match frontend expectations + return createSuccessResponse({ data: transformedProducts }); } ); diff --git a/src/app/api/analytics/revenue/route.ts b/src/app/api/analytics/revenue/route.ts index 1d2a4593..5a09b5d5 100644 --- a/src/app/api/analytics/revenue/route.ts +++ b/src/app/api/analytics/revenue/route.ts @@ -15,10 +15,22 @@ export const GET = apiHandler( return createErrorResponse('storeId is required', 400); } + // Parse toDate and set to end of day to include all orders for that day const toDate = to ? new Date(to) : new Date(); - const fromDate = from - ? new Date(from) - : new Date(toDate.getTime() - 30 * 24 * 60 * 60 * 1000); + if (to && !to.includes('T')) { + toDate.setHours(23, 59, 59, 999); + } + + // Parse fromDate and set to start of day + let fromDate: Date; + if (from) { + fromDate = new Date(from); + if (!from.includes('T')) { + fromDate.setHours(0, 0, 0, 0); + } + } else { + fromDate = new Date(toDate.getTime() - 30 * 24 * 60 * 60 * 1000); + } const analyticsService = AnalyticsService.getInstance(); const revenueData = await analyticsService.getRevenueReport(storeId, { @@ -26,6 +38,7 @@ export const GET = apiHandler( to: toDate, }); - return createSuccessResponse(revenueData); + // Wrap in { data: ... } to match frontend expectations + return createSuccessResponse({ data: revenueData }); } ); diff --git a/src/app/api/analytics/sales/route.ts b/src/app/api/analytics/sales/route.ts index 90c0bcae..6da67122 100644 --- a/src/app/api/analytics/sales/route.ts +++ b/src/app/api/analytics/sales/route.ts @@ -15,10 +15,22 @@ export const GET = apiHandler( return createErrorResponse('storeId is required', 400); } + // Parse toDate and set to end of day to include all orders for that day const toDate = to ? new Date(to) : new Date(); - const fromDate = from - ? new Date(from) - : new Date(toDate.getTime() - 30 * 24 * 60 * 60 * 1000); + if (to && !to.includes('T')) { + toDate.setHours(23, 59, 59, 999); + } + + // Parse fromDate and set to start of day + let fromDate: Date; + if (from) { + fromDate = new Date(from); + if (!from.includes('T')) { + fromDate.setHours(0, 0, 0, 0); + } + } else { + fromDate = new Date(toDate.getTime() - 30 * 24 * 60 * 60 * 1000); + } const analyticsService = AnalyticsService.getInstance(); const salesData = await analyticsService.getSalesReport(storeId, { diff --git a/src/app/api/checkout/payment-intent/route.ts b/src/app/api/checkout/payment-intent/route.ts index a3f1414a..2f670464 100644 --- a/src/app/api/checkout/payment-intent/route.ts +++ b/src/app/api/checkout/payment-intent/route.ts @@ -17,7 +17,7 @@ const PaymentIntentSchema = z.object({ storeId: cuidSchema, orderId: cuidSchema, amount: z.number().min(0), - currency: z.string().default('usd'), + currency: z.string().default('bdt'), }); export const POST = apiHandler( diff --git a/src/app/api/integrations/[id]/route.ts b/src/app/api/integrations/[id]/route.ts index ae8a260e..4e126a66 100644 --- a/src/app/api/integrations/[id]/route.ts +++ b/src/app/api/integrations/[id]/route.ts @@ -49,7 +49,7 @@ export const GET = apiHandler( settings: { publishableKey: 'pk_test_***************', webhookSecret: 'whsec_***************', - currency: 'USD', + currency: 'BDT', }, stats: { transactionsProcessed: 142, diff --git a/src/app/api/orders/[id]/invoice/route.ts b/src/app/api/orders/[id]/invoice/route.ts index c72aaf29..48e4f9d9 100644 --- a/src/app/api/orders/[id]/invoice/route.ts +++ b/src/app/api/orders/[id]/invoice/route.ts @@ -109,7 +109,7 @@ BT 50 700 Td (Customer: ${invoiceData.customer ? `${invoiceData.customer.firstName} ${invoiceData.customer.lastName}` : 'N/A'}) Tj 50 680 Td -(Total: $${invoiceData.totalAmount.toFixed(2)}) Tj +(Total: ৳${invoiceData.totalAmount.toFixed(2)}) Tj 50 660 Td (Status: ${invoiceData.paymentStatus}) Tj 50 640 Td diff --git a/src/app/api/shipping/rates/route.ts b/src/app/api/shipping/rates/route.ts index b38d4cf2..ddfd42f0 100644 --- a/src/app/api/shipping/rates/route.ts +++ b/src/app/api/shipping/rates/route.ts @@ -34,33 +34,33 @@ export const POST = apiHandler({}, async (request: NextRequest) => { // Mock shipping rates calculation const baseRate = 5.00; const perKgRate = 2.50; - const internationalMultiplier = destination.country !== 'US' ? 2.5 : 1; + const internationalMultiplier = destination.country !== 'BD' ? 2.5 : 1; const rates = [ { id: 'standard', name: 'Standard Shipping', - carrier: 'USPS', - deliveryDays: destination.country === 'US' ? '5-7' : '10-15', - price: Number(((baseRate + weight * perKgRate) * internationalMultiplier).toFixed(2)), - currency: 'USD', + carrier: 'Sundarban Courier', + deliveryDays: destination.country === 'BD' ? '3-5' : '10-15', + price: Number(((baseRate + weight * perKgRate) * internationalMultiplier * 100).toFixed(2)), + currency: 'BDT', }, { id: 'express', name: 'Express Shipping', - carrier: 'FedEx', - deliveryDays: destination.country === 'US' ? '2-3' : '5-7', - price: Number(((baseRate + weight * perKgRate) * internationalMultiplier * 2).toFixed(2)), - currency: 'USD', + carrier: 'SA Paribahan', + deliveryDays: destination.country === 'BD' ? '1-2' : '5-7', + price: Number(((baseRate + weight * perKgRate) * internationalMultiplier * 200).toFixed(2)), + currency: 'BDT', }, { id: 'overnight', name: 'Overnight Shipping', - carrier: 'UPS', + carrier: 'Pathao Express', deliveryDays: '1', - price: Number(((baseRate + weight * perKgRate) * internationalMultiplier * 4).toFixed(2)), - currency: 'USD', - available: destination.country === 'US', + price: Number(((baseRate + weight * perKgRate) * internationalMultiplier * 400).toFixed(2)), + currency: 'BDT', + available: destination.country === 'BD', }, ].filter(rate => rate.available !== false); diff --git a/src/app/api/store/[slug]/orders/route.ts b/src/app/api/store/[slug]/orders/route.ts index 58568e17..9c12f8f6 100644 --- a/src/app/api/store/[slug]/orders/route.ts +++ b/src/app/api/store/[slug]/orders/route.ts @@ -76,8 +76,8 @@ function generateOrderNumber(): string { /** * Format currency for display */ -function formatCurrency(amount: number, currency: string = 'USD'): string { - return new Intl.NumberFormat('en-US', { +function formatCurrency(amount: number, currency: string = 'BDT'): string { + return new Intl.NumberFormat('bn-BD', { style: 'currency', currency, }).format(amount); diff --git a/src/app/api/stores/[id]/settings/route.ts b/src/app/api/stores/[id]/settings/route.ts index 374e340a..5942b39a 100644 --- a/src/app/api/stores/[id]/settings/route.ts +++ b/src/app/api/stores/[id]/settings/route.ts @@ -23,7 +23,6 @@ const settingsSchema = z.object({ currency: z.string().length(3).optional(), // ISO 4217 timezone: z.string().optional(), language: z.string().length(2).optional(), // ISO 639-1 - taxRate: z.number().min(0).max(100).optional(), shippingZones: z.array(z.string()).optional(), allowGuestCheckout: z.boolean().optional(), requireAccountVerification: z.boolean().optional(), @@ -52,10 +51,9 @@ export const GET = apiHandler( storeId, storeName: 'Acme Store', storeDescription: 'Premium quality products for your everyday needs', - currency: 'USD', - timezone: 'America/New_York', - language: 'en', - taxRate: 8.5, + currency: 'BDT', + timezone: 'Asia/Dhaka', + language: 'bn', shippingZones: ['US', 'CA', 'MX'], allowGuestCheckout: true, requireAccountVerification: true, diff --git a/src/app/api/subscriptions/route.ts b/src/app/api/subscriptions/route.ts index 5b1669ce..3b43ec3f 100644 --- a/src/app/api/subscriptions/route.ts +++ b/src/app/api/subscriptions/route.ts @@ -21,8 +21,8 @@ export const GET = apiHandler( currentPeriodStart: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(), currentPeriodEnd: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(), cancelAtPeriodEnd: false, - amount: 29.99, - currency: 'USD', + amount: 2999, + currency: 'BDT', interval: 'month', }; diff --git a/src/app/api/subscriptions/status/route.ts b/src/app/api/subscriptions/status/route.ts index 87013250..a87dece1 100644 --- a/src/app/api/subscriptions/status/route.ts +++ b/src/app/api/subscriptions/status/route.ts @@ -31,8 +31,8 @@ export const GET = apiHandler({}, async (request: NextRequest) => { status: 'active', currentPeriodStart: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString(), currentPeriodEnd: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(), - amount: 29.99, - currency: 'USD', + amount: 2999, + currency: 'BDT', cancelAtPeriodEnd: false, trialEnd: null, features: [ @@ -51,7 +51,7 @@ export const GET = apiHandler({}, async (request: NextRequest) => { apiCallsLimit: 50000, }, nextBillingDate: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(), - nextBillingAmount: 29.99, + nextBillingAmount: 2999, createdAt: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000).toISOString(), }; diff --git a/src/app/api/subscriptions/subscribe/route.ts b/src/app/api/subscriptions/subscribe/route.ts index 86207137..7915afe2 100644 --- a/src/app/api/subscriptions/subscribe/route.ts +++ b/src/app/api/subscriptions/subscribe/route.ts @@ -34,10 +34,10 @@ export const POST = apiHandler({}, async (request: NextRequest) => { currentPeriodStart: new Date().toISOString(), currentPeriodEnd: new Date(Date.now() + (interval === 'monthly' ? 30 : 365) * 24 * 60 * 60 * 1000).toISOString(), trialEnd: trialDays ? new Date(Date.now() + trialDays * 24 * 60 * 60 * 1000).toISOString() : null, - amount: plan === 'basic' ? (interval === 'monthly' ? 9.99 : 99.99) : - plan === 'pro' ? (interval === 'monthly' ? 29.99 : 299.99) : - (interval === 'monthly' ? 99.99 : 999.99), - currency: 'USD', + amount: plan === 'basic' ? (interval === 'monthly' ? 999 : 9999) : + plan === 'pro' ? (interval === 'monthly' ? 2999 : 29999) : + (interval === 'monthly' ? 9999 : 99999), + currency: 'BDT', createdAt: new Date().toISOString(), }; diff --git a/src/app/checkout/confirmation/page.tsx b/src/app/checkout/confirmation/page.tsx index ec2460b8..897b1ac6 100644 --- a/src/app/checkout/confirmation/page.tsx +++ b/src/app/checkout/confirmation/page.tsx @@ -84,7 +84,7 @@ function ConfirmationContent() {
Order Total - $118.78 + ৳11,878
diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx index b69a3673..1fb42c50 100644 --- a/src/app/checkout/page.tsx +++ b/src/app/checkout/page.tsx @@ -208,26 +208,20 @@ export default function CheckoutPage() {
2 items - $99.98 + ৳9,998
Shipping - {currentStep >= 1 && shippingAddress ? '$10.00' : 'Calculated at next step'} - -
-
- Tax - - {currentStep >= 1 && shippingAddress ? '$8.80' : 'Calculated at next step'} + {currentStep >= 1 && shippingAddress ? '৳1,000' : 'Calculated at next step'}
Total - {currentStep >= 1 && shippingAddress ? '$118.78' : '$99.98'} + {currentStep >= 1 && shippingAddress ? '৳10,998' : '৳9,998'}
diff --git a/src/app/store/[slug]/cart/page.tsx b/src/app/store/[slug]/cart/page.tsx index 0023d87d..37740557 100644 --- a/src/app/store/[slug]/cart/page.tsx +++ b/src/app/store/[slug]/cart/page.tsx @@ -27,7 +27,6 @@ export default function CartPage() { const removeItem = useCart((state) => state.removeItem); const clearCart = useCart((state) => state.clearCart); const getSubtotal = useCart((state) => state.getSubtotal); - const getEstimatedTax = useCart((state) => state.getEstimatedTax); const getEstimatedShipping = useCart((state) => state.getEstimatedShipping); const getTotal = useCart((state) => state.getTotal); @@ -55,7 +54,6 @@ export default function CartPage() { // Calculate totals const subtotal = getSubtotal(); - const estimatedTax = getEstimatedTax(); const estimatedShipping = getEstimatedShipping(); const total = getTotal(); @@ -129,13 +127,7 @@ export default function CartPage() { {/* Subtotal */}
Subtotal - ${subtotal.toFixed(2)} -
- - {/* Estimated Tax */} -
- Estimated Tax - ${estimatedTax.toFixed(2)} + ৳{subtotal.toFixed(2)}
{/* Shipping */} @@ -145,15 +137,15 @@ export default function CartPage() { {estimatedShipping === 0 ? ( FREE ) : ( - `$${estimatedShipping.toFixed(2)}` + `৳${estimatedShipping.toFixed(2)}` )} {/* Free Shipping Notice */} - {subtotal < 50 && subtotal > 0 && ( + {subtotal < 5000 && subtotal > 0 && (
- Add ${(50 - subtotal).toFixed(2)} more to qualify for free shipping! + Add ৳{(5000 - subtotal).toFixed(2)} more to qualify for free shipping!
)} @@ -162,7 +154,7 @@ export default function CartPage() { {/* Total */}
Total - ${total.toFixed(2)} + ৳{total.toFixed(2)}
{/* Checkout Button */} diff --git a/src/app/store/[slug]/checkout/page.tsx b/src/app/store/[slug]/checkout/page.tsx index 5b14d81c..2ab44c6d 100644 --- a/src/app/store/[slug]/checkout/page.tsx +++ b/src/app/store/[slug]/checkout/page.tsx @@ -115,7 +115,6 @@ export default function CheckoutPage() { const setStoreSlug = useCart((state) => state.setStoreSlug); const clearCart = useCart((state) => state.clearCart); const getSubtotal = useCart((state) => state.getSubtotal); - const getEstimatedTax = useCart((state) => state.getEstimatedTax); const getEstimatedShipping = useCart((state) => state.getEstimatedShipping); const getTotal = useCart((state) => state.getTotal); @@ -154,7 +153,6 @@ export default function CheckoutPage() { // Calculate totals const subtotal = getSubtotal(); - const tax = getEstimatedTax(); const shipping = getEstimatedShipping(); const total = getTotal(); @@ -202,7 +200,7 @@ export default function CheckoutPage() { price: item.price, })), subtotal, - taxAmount: tax, + taxAmount: 0, shippingAmount: shipping, totalAmount: total, paymentMethod: data.paymentMethod, @@ -631,11 +629,11 @@ export default function CheckoutPage() {

{item.productName}

- Qty: {item.quantity} × ${item.price.toFixed(2)} + Qty: {item.quantity} × ৳{item.price.toFixed(2)}

- ${(item.price * item.quantity).toFixed(2)} + ৳{(item.price * item.quantity).toFixed(2)} ))} @@ -647,7 +645,7 @@ export default function CheckoutPage() {
Subtotal - ${subtotal.toFixed(2)} + ৳{subtotal.toFixed(2)}
Shipping @@ -655,14 +653,10 @@ export default function CheckoutPage() { {shipping === 0 ? ( FREE ) : ( - `$${shipping.toFixed(2)}` + `৳${shipping.toFixed(2)}` )}
-
- Tax (estimated) - ${tax.toFixed(2)} -
@@ -670,7 +664,7 @@ export default function CheckoutPage() { {/* Total */}
Total - ${total.toFixed(2)} + ৳{total.toFixed(2)}
{/* Submit Button */} diff --git a/src/app/store/[slug]/components/cart-item.tsx b/src/app/store/[slug]/components/cart-item.tsx index 7b41993c..d1ceaeed 100644 --- a/src/app/store/[slug]/components/cart-item.tsx +++ b/src/app/store/[slug]/components/cart-item.tsx @@ -90,7 +90,7 @@ export function CartItemComponent({ )}

- ${item.price.toFixed(2)} each + ৳{item.price.toFixed(2)} each

{/* Quantity Controls - Mobile */} @@ -161,7 +161,7 @@ export function CartItemComponent({ {/* Item Total */}
-

${itemTotal.toFixed(2)}

+

৳{itemTotal.toFixed(2)}

{/* Remove Button */} @@ -177,7 +177,7 @@ export function CartItemComponent({ {/* Item Total - Mobile */}
-

${itemTotal.toFixed(2)}

+

৳{itemTotal.toFixed(2)}

); diff --git a/src/app/store/[slug]/components/price-display.tsx b/src/app/store/[slug]/components/price-display.tsx index 9a1cf6af..ba19cd26 100644 --- a/src/app/store/[slug]/components/price-display.tsx +++ b/src/app/store/[slug]/components/price-display.tsx @@ -40,7 +40,7 @@ export function PriceDisplay({ return (
- ${price.toFixed(2)} + ৳{price.toFixed(2)} {isOnSale && ( @@ -51,7 +51,7 @@ export function PriceDisplay({ compareSizeClasses[size] )} > - ${compareAtPrice.toFixed(2)} + ৳{compareAtPrice.toFixed(2)} {showDiscount && discountPercentage > 0 && ( diff --git a/src/app/store/[slug]/products/[productSlug]/page.tsx b/src/app/store/[slug]/products/[productSlug]/page.tsx index 897d2c9b..ffb83cfe 100644 --- a/src/app/store/[slug]/products/[productSlug]/page.tsx +++ b/src/app/store/[slug]/products/[productSlug]/page.tsx @@ -260,7 +260,7 @@ export default async function StoreProductPage({ params }: StoreProductPageProps

Free Shipping

- On orders over $50 + On orders over ৳5,000

diff --git a/src/components/admin/admin-dashboard.tsx b/src/components/admin/admin-dashboard.tsx index ad2f8b6e..5d37552b 100644 --- a/src/components/admin/admin-dashboard.tsx +++ b/src/components/admin/admin-dashboard.tsx @@ -116,12 +116,12 @@ export function AdminDashboard() { -
${(stats.revenue?.total || 0).toLocaleString()}
+
৳{(stats.revenue?.total || 0).toLocaleString()}

+{stats.revenue?.growth || 0}% from last month

- ${(stats.revenue?.monthly || 0).toLocaleString()} this month + ৳{(stats.revenue?.monthly || 0).toLocaleString()} this month

diff --git a/src/components/admin/store-request-actions.tsx b/src/components/admin/store-request-actions.tsx index 78c9a3ce..c751a554 100644 --- a/src/components/admin/store-request-actions.tsx +++ b/src/components/admin/store-request-actions.tsx @@ -146,9 +146,9 @@ export function StoreRequestActions({ requestId, storeName }: StoreRequestAction Free - Basic - $29/mo - Pro - $79/mo - Enterprise - $199/mo + Basic - ৳2,900/mo + Pro - ৳7,900/mo + Enterprise - ৳19,900/mo

diff --git a/src/components/analytics-dashboard.tsx b/src/components/analytics-dashboard.tsx index bfd0428f..b47dd481 100644 --- a/src/components/analytics-dashboard.tsx +++ b/src/components/analytics-dashboard.tsx @@ -95,9 +95,9 @@ export function AnalyticsDashboard({ storeId, dateRange }: AnalyticsDashboardPro // Format currency const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat('bn-BD', { style: 'currency', - currency: 'USD', + currency: 'BDT', minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(amount); diff --git a/src/components/analytics/analytics-dashboard.tsx b/src/components/analytics/analytics-dashboard.tsx index b7e5cd5e..0ce0accd 100644 --- a/src/components/analytics/analytics-dashboard.tsx +++ b/src/components/analytics/analytics-dashboard.tsx @@ -90,11 +90,15 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP const [loading, setLoading] = useState(true); const [timeRange, setTimeRange] = useState('30d'); const [selectedStoreId, setSelectedStoreId] = useState(propStoreId || ''); + // Track the active time range for which we have data + const [activeTimeRange, setActiveTimeRange] = useState('30d'); // Get storeId from props, state, or session const storeId = propStoreId || selectedStoreId || (session?.user as { storeId?: string })?.storeId; useEffect(() => { + const abortController = new AbortController(); + const fetchMetrics = async () => { if (!storeId) { setLoading(false); @@ -102,6 +106,8 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP } setLoading(true); + // Clear metrics when time range changes to show fresh loading state + setMetrics(null); try { const { from, to } = getDateRange(timeRange); const params = new URLSearchParams({ @@ -110,26 +116,39 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP to, }); - const response = await fetch(`/api/analytics/dashboard?${params}`); + console.log(`[Analytics] Fetching dashboard for ${timeRange}: ${from} to ${to}`); + + const response = await fetch(`/api/analytics/dashboard?${params}`, { + signal: abortController.signal, + cache: 'no-store', + }); if (!response.ok) throw new Error('Failed to fetch analytics'); const data = await response.json(); + console.log(`[Analytics] Received dashboard data:`, data); setMetrics(data); + setActiveTimeRange(timeRange); } catch (error) { - toast.error('Failed to load analytics data'); - console.error('Analytics error:', error); + if ((error as Error).name !== 'AbortError') { + toast.error('Failed to load analytics data'); + console.error('Analytics error:', error); + } } finally { setLoading(false); } }; fetchMetrics(); + + return () => { + abortController.abort(); + }; }, [timeRange, storeId]); const formatCurrency = (value: number) => { - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat('bn-BD', { style: 'currency', - currency: 'USD', + currency: 'BDT', }).format(value); }; @@ -156,6 +175,9 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP ); } +// Show loading skeleton when fetching data + const isRefreshing = loading || !metrics; + return (

{/* Store Selector */} @@ -175,20 +197,24 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP Last year - - {/* Metric Cards */} -
- + + {/* Metric Cards - use key to force re-render on time range change */} +
+ Total Revenue
- {metrics ? formatCurrency(metrics.revenue.total) : '---'} + {isRefreshing ? ( + Loading... + ) : metrics ? ( + formatCurrency(metrics.revenue.total) + ) : '---'}

- {metrics && ( + {!isRefreshing && metrics && ( 0 ? 'text-green-600' : 'text-red-600'}> {formatChange(metrics.revenue.change)} from last period @@ -197,17 +223,19 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP - + Orders

- {metrics?.orders.total.toLocaleString() || '---'} + {isRefreshing ? ( + Loading... + ) : metrics?.orders.total.toLocaleString() || '---'}

- {metrics && metrics.orders.change !== undefined && ( + {!isRefreshing && metrics && metrics.orders.change !== undefined && ( 0 ? 'text-green-600' : 'text-red-600'}> {formatChange(metrics.orders.change)} from last period @@ -216,17 +244,19 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP - + Customers

- {metrics?.customers.total.toLocaleString() || '---'} + {isRefreshing ? ( + Loading... + ) : metrics?.customers.total.toLocaleString() || '---'}

- {metrics && metrics.customers.change !== undefined && ( + {!isRefreshing && metrics && metrics.customers.change !== undefined && ( 0 ? 'text-green-600' : 'text-red-600'}> {formatChange(metrics.customers.change)} from last period @@ -235,14 +265,16 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP - + Products

- {metrics?.products?.total?.toLocaleString() || '---'} + {isRefreshing ? ( + Loading... + ) : metrics?.products?.total?.toLocaleString() || '---'}

Active products in store @@ -261,7 +293,7 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP - + @@ -273,7 +305,7 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP - +

@@ -287,7 +319,7 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP - +
diff --git a/src/components/analytics/customer-metrics.tsx b/src/components/analytics/customer-metrics.tsx index 11ae50f3..33ad7e66 100644 --- a/src/components/analytics/customer-metrics.tsx +++ b/src/components/analytics/customer-metrics.tsx @@ -74,10 +74,10 @@ export function CustomerMetrics({ storeId, timeRange }: CustomerMetricsProps) { }, [storeId, timeRange]); const formatCurrency = (value: number | undefined | null) => { - if (value === null || value === undefined) return '$0.00'; - return new Intl.NumberFormat('en-US', { + if (value === null || value === undefined) return '৳0.00'; + return new Intl.NumberFormat('bn-BD', { style: 'currency', - currency: 'USD', + currency: 'BDT', }).format(value); }; diff --git a/src/components/analytics/revenue-chart.tsx b/src/components/analytics/revenue-chart.tsx index 9d24c7f3..ab8beb87 100644 --- a/src/components/analytics/revenue-chart.tsx +++ b/src/components/analytics/revenue-chart.tsx @@ -88,9 +88,9 @@ export function RevenueChart({ storeId, timeRange }: RevenueChartProps) { } const formatCurrency = (value: number) => { - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat('bn-BD', { style: 'currency', - currency: 'USD', + currency: 'BDT', minimumFractionDigits: 0, }).format(value); }; diff --git a/src/components/analytics/top-products-table.tsx b/src/components/analytics/top-products-table.tsx index 1d10c767..17203864 100644 --- a/src/components/analytics/top-products-table.tsx +++ b/src/components/analytics/top-products-table.tsx @@ -57,9 +57,9 @@ export function TopProductsTable({ storeId, timeRange }: TopProductsTableProps) }, [storeId, timeRange]); const formatCurrency = (value: number) => { - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat('bn-BD', { style: 'currency', - currency: 'USD', + currency: 'BDT', minimumFractionDigits: 0, }).format(value); }; diff --git a/src/components/cart/cart-drawer.tsx b/src/components/cart/cart-drawer.tsx index ef8b8160..cff23dfb 100644 --- a/src/components/cart/cart-drawer.tsx +++ b/src/components/cart/cart-drawer.tsx @@ -67,7 +67,7 @@ interface CartDrawerProps { /** * Format price with currency */ -function formatPrice(price: number, currency: string = "$"): string { +function formatPrice(price: number, currency: string = "৳"): string { return `${currency}${price.toFixed(2)}`; } @@ -92,7 +92,7 @@ function formatPrice(price: number, currency: string = "$"): string { */ export function CartDrawer({ items, - currency = "$", + currency = "৳", onQuantityChange, onRemoveItem, onCheckout, @@ -326,7 +326,7 @@ export function CartDrawer({

- Shipping and taxes calculated at checkout + Shipping calculated at checkout

{/* Checkout Button */} diff --git a/src/components/cart/cart-list.tsx b/src/components/cart/cart-list.tsx index 2610e431..0c836af0 100644 --- a/src/components/cart/cart-list.tsx +++ b/src/components/cart/cart-list.tsx @@ -31,7 +31,6 @@ interface Cart { id: string; items: CartItem[]; subtotal: number; - tax: number; shipping: number; total: number; itemCount: number; @@ -58,9 +57,8 @@ const mockCart: Cart = { }, ], subtotal: 459.97, - tax: 36.80, shipping: 10.00, - total: 506.77, + total: 469.97, itemCount: 3, }; @@ -241,7 +239,7 @@ export function CartList() {

{item.productName}

- ${item.price.toFixed(2)} each + ৳{item.price.toFixed(2)} each

@@ -304,20 +302,16 @@ export function CartList() {
Subtotal - ${cart.subtotal.toFixed(2)} -
-
- Tax - ${cart.tax.toFixed(2)} + ৳{cart.subtotal.toFixed(2)}
Shipping - ${cart.shipping.toFixed(2)} + ৳{cart.shipping.toFixed(2)}
Total - ${cart.total.toFixed(2)} + ৳{cart.total.toFixed(2)}
diff --git a/src/components/checkout/cart-review-step.tsx b/src/components/checkout/cart-review-step.tsx index 72228b1e..5d385c10 100644 --- a/src/components/checkout/cart-review-step.tsx +++ b/src/components/checkout/cart-review-step.tsx @@ -191,9 +191,9 @@ export function CartReviewStep({ onNext, isProcessing }: CartReviewStepProps) { {/* Price */}
-

${(item.price * item.quantity).toFixed(2)}

+

৳{(item.price * item.quantity).toFixed(2)}

- ${item.price.toFixed(2)} each + ৳{item.price.toFixed(2)} each

@@ -206,10 +206,10 @@ export function CartReviewStep({ onNext, isProcessing }: CartReviewStepProps) {
Subtotal ({cartItems.length} items) - ${subtotal.toFixed(2)} + ৳{subtotal.toFixed(2)}

- Shipping and taxes calculated at next step + Shipping calculated at next step

diff --git a/src/components/checkout/payment-method-step.tsx b/src/components/checkout/payment-method-step.tsx index 2d1d1277..2b1b280b 100644 --- a/src/components/checkout/payment-method-step.tsx +++ b/src/components/checkout/payment-method-step.tsx @@ -40,8 +40,8 @@ export function PaymentMethodStep({ // method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ - // amount: 11878, // $118.78 in cents - // currency: 'usd', + // amount: 11878, // ৳118.78 in paisa + // currency: 'bdt', // }), // }); @@ -221,20 +221,16 @@ export function PaymentMethodStep({
Subtotal - $99.98 + ৳9,998
Shipping - $10.00 -
-
- Tax - $8.80 + ৳1,000
Total - $118.78 + ৳10,998
diff --git a/src/components/customers/customer-detail-dialog.tsx b/src/components/customers/customer-detail-dialog.tsx index cc41d5e0..80715ccb 100644 --- a/src/components/customers/customer-detail-dialog.tsx +++ b/src/components/customers/customer-detail-dialog.tsx @@ -44,9 +44,9 @@ export function CustomerDetailDialog({ onOpenChange, }: CustomerDetailDialogProps) { const formatCurrency = (value: number) => { - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat('bn-BD', { style: 'currency', - currency: 'USD', + currency: 'BDT', }).format(value); }; diff --git a/src/components/customers/customers-list.tsx b/src/components/customers/customers-list.tsx index d00e5651..4445b354 100644 --- a/src/components/customers/customers-list.tsx +++ b/src/components/customers/customers-list.tsx @@ -150,9 +150,9 @@ export function CustomersList({ storeId }: CustomersListProps) { }; const formatCurrency = (value: number) => { - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat('bn-BD', { style: 'currency', - currency: 'USD', + currency: 'BDT', }).format(value); }; diff --git a/src/components/dashboard/storefront/trust-badges-editor.tsx b/src/components/dashboard/storefront/trust-badges-editor.tsx index f6379dee..33a20591 100644 --- a/src/components/dashboard/storefront/trust-badges-editor.tsx +++ b/src/components/dashboard/storefront/trust-badges-editor.tsx @@ -156,7 +156,7 @@ export function TrustBadgesEditor({ onChange={(e) => updateBadge(badge.id, { description: e.target.value }) } - placeholder="On orders over $50" + placeholder="On orders over ৳5,000" /> diff --git a/src/components/dashboards/store-admin-dashboard.tsx b/src/components/dashboards/store-admin-dashboard.tsx index 76a30062..5cc1d8ec 100644 --- a/src/components/dashboards/store-admin-dashboard.tsx +++ b/src/components/dashboards/store-admin-dashboard.tsx @@ -134,7 +134,7 @@ export function StoreAdminDashboard({ storeId }: StoreAdminDashboardProps) { diff --git a/src/components/dashboards/super-admin-dashboard.tsx b/src/components/dashboards/super-admin-dashboard.tsx index bba81b2a..3f15c6b9 100644 --- a/src/components/dashboards/super-admin-dashboard.tsx +++ b/src/components/dashboards/super-admin-dashboard.tsx @@ -137,8 +137,8 @@ export function SuperAdminDashboard() { diff --git a/src/components/notifications/notifications-list.tsx b/src/components/notifications/notifications-list.tsx index bb27b540..410df2a6 100644 --- a/src/components/notifications/notifications-list.tsx +++ b/src/components/notifications/notifications-list.tsx @@ -30,7 +30,7 @@ const mockNotifications: Notification[] = [ id: 'not1', type: 'order', title: 'New Order Received', - message: 'Order #ORD-2024-123 for $249.99 has been placed.', + message: 'Order #ORD-2024-123 for ৳24,999 has been placed.', read: false, createdAt: new Date(Date.now() - 10 * 60 * 1000).toISOString(), priority: 'high', diff --git a/src/components/order-detail-client.tsx b/src/components/order-detail-client.tsx index 82f9cd22..f923ba70 100644 --- a/src/components/order-detail-client.tsx +++ b/src/components/order-detail-client.tsx @@ -312,9 +312,9 @@ export function OrderDetailClient({ orderId, storeId }: OrderDetailClientProps) // Format currency const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat('bn-BD', { style: 'currency', - currency: 'USD', + currency: 'BDT', }).format(amount); }; @@ -463,10 +463,6 @@ export function OrderDetailClient({ orderId, storeId }: OrderDetailClientProps) Shipping {formatCurrency(order.shippingAmount)} -
- Tax - {formatCurrency(order.taxAmount)} -
{order.discountAmount > 0 && (
Discount diff --git a/src/components/orders-table.tsx b/src/components/orders-table.tsx index b16ae46c..dbd110ac 100644 --- a/src/components/orders-table.tsx +++ b/src/components/orders-table.tsx @@ -150,9 +150,9 @@ const paymentStatusColors: Record = { // Format currency const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('en-US', { + return new Intl.NumberFormat('bn-BD', { style: 'currency', - currency: 'USD', + currency: 'BDT', }).format(amount); }; diff --git a/src/components/orders/refund-dialog.tsx b/src/components/orders/refund-dialog.tsx index 137b5836..ccea3454 100644 --- a/src/components/orders/refund-dialog.tsx +++ b/src/components/orders/refund-dialog.tsx @@ -54,7 +54,7 @@ export function RefundDialog({ const refundableBalance = totalAmount - refundedAmount; const formatCurrency = (amount: number) => - new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); + new Intl.NumberFormat('bn-BD', { style: 'currency', currency: 'BDT' }).format(amount); const handleRefund = async () => { setError(''); diff --git a/src/components/price-range-filter.tsx b/src/components/price-range-filter.tsx index 1d5053e4..186b258e 100644 --- a/src/components/price-range-filter.tsx +++ b/src/components/price-range-filter.tsx @@ -50,7 +50,7 @@ export function PriceRangeFilter({ onChange, onCommit, step = 1, - currency = "USD", + currency = "BDT", showInputs = true, showReset = true, className, @@ -225,7 +225,7 @@ export function PriceRangeFilterCompact({ onChange, onCommit, step = 1, - currency = "USD", + currency = "BDT", disabled = false, }: Omit< PriceRangeFilterProps, diff --git a/src/components/product/variant-manager.tsx b/src/components/product/variant-manager.tsx index 464f473c..6508d22d 100644 --- a/src/components/product/variant-manager.tsx +++ b/src/components/product/variant-manager.tsx @@ -431,7 +431,7 @@ export function VariantManager({ {variant.price !== null - ? `$${variant.price.toFixed(2)}` + ? `৳${variant.price.toFixed(2)}` : '—'} diff --git a/src/components/products-table.tsx b/src/components/products-table.tsx index 03408c92..9370a5de 100644 --- a/src/components/products-table.tsx +++ b/src/components/products-table.tsx @@ -555,7 +555,7 @@ export function ProductsTable({ {product.sku} - ${product.price.toFixed(2)} + ৳{product.price.toFixed(2)}
diff --git a/src/components/products/price-range-filter.tsx b/src/components/products/price-range-filter.tsx index d6332ace..c768d54a 100644 --- a/src/components/products/price-range-filter.tsx +++ b/src/components/products/price-range-filter.tsx @@ -54,7 +54,7 @@ interface PriceRangeFilterProps { /** * Format price with currency */ -function formatPrice(price: number, currency: string = "$"): string { +function formatPrice(price: number, currency: string = "৳"): string { return `${currency}${price.toLocaleString()}`; } @@ -321,11 +321,11 @@ export function PriceRangeFilter({ * Pre-defined price ranges for quick selection */ export const PRESET_PRICE_RANGES = [ - { label: "Under $25", range: { min: 0, max: 25 } }, - { label: "$25 to $50", range: { min: 25, max: 50 } }, - { label: "$50 to $100", range: { min: 50, max: 100 } }, - { label: "$100 to $200", range: { min: 100, max: 200 } }, - { label: "Over $200", range: { min: 200, max: 10000 } }, + { label: "Under ৳2,500", range: { min: 0, max: 2500 } }, + { label: "৳2,500 to ৳5,000", range: { min: 2500, max: 5000 } }, + { label: "৳5,000 to ৳10,000", range: { min: 5000, max: 10000 } }, + { label: "৳10,000 to ৳20,000", range: { min: 10000, max: 20000 } }, + { label: "Over ৳20,000", range: { min: 20000, max: 1000000 } }, ] as const; /** diff --git a/src/components/products/product-quick-view.tsx b/src/components/products/product-quick-view.tsx index 3b8e190d..cb6b3852 100644 --- a/src/components/products/product-quick-view.tsx +++ b/src/components/products/product-quick-view.tsx @@ -59,7 +59,7 @@ interface ProductQuickViewProps { /** * Format price with currency */ -function formatPrice(price: number, currency: string = "$"): string { +function formatPrice(price: number, currency: string = "৳"): string { return `${currency}${price.toFixed(2)}`; } diff --git a/src/components/section-cards.tsx b/src/components/section-cards.tsx index f714d256..e9e96929 100644 --- a/src/components/section-cards.tsx +++ b/src/components/section-cards.tsx @@ -17,7 +17,7 @@ export function SectionCards() { Total Revenue - $1,250.00 + ৳125,000 diff --git a/src/components/storefront/discount-banner.tsx b/src/components/storefront/discount-banner.tsx index b57d620f..65249bbd 100644 --- a/src/components/storefront/discount-banner.tsx +++ b/src/components/storefront/discount-banner.tsx @@ -5,9 +5,12 @@ * * Customizable promotional banner for storefronts. * Can be positioned at top or bottom, and optionally dismissible. + * + * Note: This component uses client-side state for localStorage and Date checks + * to avoid hydration mismatches. */ -import { useState } from "react"; +import { useState, useEffect } from "react"; import Link from "next/link"; import { X } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -20,7 +23,7 @@ interface DiscountBannerProps { } /** - * Check if banner is expired + * Check if banner is expired (client-side only to avoid hydration mismatch) */ function isBannerExpired(expiresAt?: string): boolean { if (!expiresAt) return false; @@ -31,7 +34,6 @@ function isBannerExpired(expiresAt?: string): boolean { * Check if banner was previously dismissed (client-side only) */ function wasBannerDismissed(bannerId: string, storeSlug: string): boolean { - if (typeof window === 'undefined') return false; try { const dismissedBanners = localStorage.getItem(`dismissed_banners_${storeSlug}`); if (dismissedBanners) { @@ -48,12 +50,26 @@ function wasBannerDismissed(bannerId: string, storeSlug: string): boolean { * Single discount banner component */ export function DiscountBanner({ banner, storeSlug }: DiscountBannerProps) { - const [isDismissed, setIsDismissed] = useState(() => - wasBannerDismissed(banner.id, storeSlug) + // Track mounted state for client-side checks + const [isMounted, setIsMounted] = useState(false); + const [isDismissed, setIsDismissed] = useState(false); + + // Set mounted state on client after hydration + // This pattern is intentional to avoid hydration mismatch with localStorage/Date checks + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect + setIsMounted(true); + }, []); + + // Check dismissal status only when mounted (client-side only) + const shouldHide = isMounted && ( + isDismissed || + wasBannerDismissed(banner.id, storeSlug) || + isBannerExpired(banner.expiresAt) ); - // Don't render if disabled, dismissed, or expired - if (!banner.enabled || isDismissed || isBannerExpired(banner.expiresAt)) { + // Don't render if disabled or hidden (dismissed/expired checked client-side) + if (!banner.enabled || shouldHide) { return null; } diff --git a/src/components/storefront/product-grid.tsx b/src/components/storefront/product-grid.tsx index bbeae1b7..2ec635bb 100644 --- a/src/components/storefront/product-grid.tsx +++ b/src/components/storefront/product-grid.tsx @@ -98,7 +98,7 @@ function ProductCard({ product, storeSlug, showCategory }: {

)}
- ${product.price.toFixed(2)} + ৳{product.price.toFixed(2)} {hasDiscount && ( {discountPercent}% OFF @@ -158,11 +158,11 @@ function ProductCard({ product, storeSlug, showCategory }: { {/* Price Section */}
- ${product.price.toFixed(2)} + ৳{product.price.toFixed(2)} {hasDiscount && ( - ${product.compareAtPrice!.toFixed(2)} + ৳{product.compareAtPrice!.toFixed(2)} )}
diff --git a/src/components/stores/store-form-dialog.tsx b/src/components/stores/store-form-dialog.tsx index 15149f2c..91a1b15c 100644 --- a/src/components/stores/store-form-dialog.tsx +++ b/src/components/stores/store-form-dialog.tsx @@ -59,8 +59,7 @@ const storeFormSchema = z.object({ subscriptionStatus: z.enum(['TRIALING', 'ACTIVE', 'PAST_DUE', 'CANCELED', 'UNPAID']), isActive: z.boolean().default(true), settings: z.object({ - currency: z.string().default('USD'), - taxRate: z.number().min(0).max(100).default(0), + currency: z.string().default('BDT'), }).optional(), }); @@ -80,7 +79,6 @@ interface Store { isActive: boolean; settings?: { currency?: string; - taxRate?: number; }; } @@ -110,8 +108,7 @@ export function StoreFormDialog({ open, onOpenChange, store, onSuccess }: StoreF subscriptionStatus: 'TRIALING', isActive: true, settings: { - currency: 'USD', - taxRate: 0, + currency: 'BDT', }, }, }); @@ -130,8 +127,7 @@ export function StoreFormDialog({ open, onOpenChange, store, onSuccess }: StoreF subscriptionStatus: store.subscriptionStatus as 'TRIALING' | 'ACTIVE' | 'PAST_DUE' | 'CANCELED' | 'UNPAID', isActive: store.isActive, settings: { - currency: store.settings?.currency || 'USD', - taxRate: store.settings?.taxRate || 0, + currency: store.settings?.currency || 'BDT', }, }); } @@ -350,37 +346,17 @@ export function StoreFormDialog({ open, onOpenChange, store, onSuccess }: StoreF + BDT - Bangladeshi Taka USD - US Dollar EUR - Euro GBP - British Pound - CAD - Canadian Dollar + INR - Indian Rupee )} /> - - ( - - Tax Rate (%) - - field.onChange(parseFloat(e.target.value) || 0)} - /> - - - - )} - />
({ }, [columns, enableRowSelection]); // React Compiler note: disable the incompatible-library check for useReactTable - // eslint-disable-next-line react-hooks/incompatible-library + const table = useReactTable({ data, columns: tableColumns, diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 59de33c8..b10a52fc 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -4,6 +4,7 @@ import { PrismaPg } from "@prisma/adapter-pg"; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; queryMetrics: QueryMetrics | undefined; + listenersAttached: boolean | undefined; }; // Query metrics for monitoring @@ -45,7 +46,9 @@ const queryMetrics: QueryMetrics = globalForPrisma.queryMetrics ?? { // Track slow queries in development (>100ms threshold) const SLOW_QUERY_THRESHOLD_MS = 100; -if (process.env.NODE_ENV === "development") { +if (process.env.NODE_ENV === "development" && !globalForPrisma.listenersAttached) { + globalForPrisma.listenersAttached = true; + // @ts-expect-error - Prisma types for $on are not well-defined for 'query' events prisma.$on("query", (e: { query: string; duration: number }) => { queryMetrics.total++; diff --git a/src/lib/services/analytics.service.ts b/src/lib/services/analytics.service.ts index d4de6887..02bcf80f 100644 --- a/src/lib/services/analytics.service.ts +++ b/src/lib/services/analytics.service.ts @@ -427,7 +427,7 @@ export class AnalyticsService { }); // Get customers who made orders in this period - const customersWithOrders = await prisma.order.findMany({ + const customersWithOrdersInPeriod = await prisma.order.findMany({ where: { storeId, createdAt: { @@ -442,11 +442,11 @@ export class AnalyticsService { distinct: ['customerId'], }); - const customerIdsInPeriod = customersWithOrders + const customerIdsInPeriod = customersWithOrdersInPeriod .map(o => o.customerId) .filter((id): id is string => id !== null); - // Get returning customers (those who had orders before this period) + // Get returning customers (those who had orders before this period AND ordered in this period) let returningCustomers = 0; if (customerIdsInPeriod.length > 0) { const customersWithPreviousOrders = await prisma.order.findMany({ @@ -468,11 +468,15 @@ export class AnalyticsService { returningCustomers = customersWithPreviousOrders.length; } - // Calculate retention rate (returning customers / customers from previous period) - const previousPeriodCustomers = await prisma.order.findMany({ + // Get customers who ordered in previous period (for retention calculation) + const periodLength = endDate.getTime() - startDate.getTime(); + const previousPeriodStart = new Date(startDate.getTime() - periodLength); + + const customersInPreviousPeriod = await prisma.order.findMany({ where: { storeId, createdAt: { + gte: previousPeriodStart, lt: startDate, }, deletedAt: null, @@ -483,15 +487,66 @@ export class AnalyticsService { distinct: ['customerId'], }); - const customerRetentionRate = previousPeriodCustomers.length > 0 - ? (returningCustomers / previousPeriodCustomers.length) * 100 + const previousPeriodCustomerIds = customersInPreviousPeriod + .map(o => o.customerId) + .filter((id): id is string => id !== null); + + // Retention: customers from previous period who also ordered in current period + let retainedCustomers = 0; + if (previousPeriodCustomerIds.length > 0) { + const retained = customerIdsInPeriod.filter(id => previousPeriodCustomerIds.includes(id)); + retainedCustomers = retained.length; + } + + // Calculate retention rate + const customerRetentionRate = previousPeriodCustomerIds.length > 0 + ? (retainedCustomers / previousPeriodCustomerIds.length) * 100 + : (totalCustomers > 0 ? 100 : 0); // If no previous period, assume 100% retention if we have customers + + // Calculate Average Lifetime Value (total revenue / customers with orders) + const revenueData = await prisma.order.aggregate({ + where: { + storeId, + status: { + in: [OrderStatus.PAID, OrderStatus.PROCESSING, OrderStatus.SHIPPED, OrderStatus.DELIVERED], + }, + deletedAt: null, + }, + _sum: { + totalAmount: true, + }, + }); + + const totalRevenue = Number(revenueData._sum.totalAmount ?? 0); + + // Get unique customers who have made orders + const customersWithOrders = await prisma.order.findMany({ + where: { + storeId, + deletedAt: null, + customerId: { not: null }, + }, + select: { + customerId: true, + }, + distinct: ['customerId'], + }); + + const uniqueCustomersWithOrders = customersWithOrders.length; + const avgLifetimeValue = uniqueCustomersWithOrders > 0 + ? totalRevenue / uniqueCustomersWithOrders : 0; + // Churn rate: percentage of customers who didn't return + const churnRate = 100 - customerRetentionRate; + return { totalCustomers, newCustomers, returningCustomers, customerRetentionRate: Math.round(customerRetentionRate * 100) / 100, + avgLifetimeValue: Math.round(avgLifetimeValue * 100) / 100, + churnRate: Math.max(0, Math.round(churnRate * 100) / 100), }; } diff --git a/src/lib/services/checkout.service.ts b/src/lib/services/checkout.service.ts index eefd2c83..7ae2efd6 100644 --- a/src/lib/services/checkout.service.ts +++ b/src/lib/services/checkout.service.ts @@ -281,7 +281,7 @@ export class CheckoutService { }, ]; - // Add free shipping for domestic orders over $50 + // Add free shipping for domestic orders over ৳5,000 if (isDomestic && cartSubtotal >= 50) { options.push({ id: 'free', diff --git a/src/lib/services/discount.service.ts b/src/lib/services/discount.service.ts index ec5b002c..8e4e2596 100644 --- a/src/lib/services/discount.service.ts +++ b/src/lib/services/discount.service.ts @@ -128,7 +128,7 @@ export class DiscountService { if (discount.minOrderAmount !== null && orderSubtotal < discount.minOrderAmount) { return { valid: false, - error: `Minimum order amount of $${discount.minOrderAmount.toFixed(2)} required for this code`, + error: `Minimum order amount of ৳${discount.minOrderAmount.toFixed(2)} required for this code`, }; } diff --git a/src/lib/services/order-processing.service.ts b/src/lib/services/order-processing.service.ts index aa75549d..9ae86caa 100644 --- a/src/lib/services/order-processing.service.ts +++ b/src/lib/services/order-processing.service.ts @@ -49,6 +49,9 @@ export class OrderProcessingService { /** * Create order atomically with inventory decrement * Uses Prisma transaction to ensure data consistency + * + * Note: Extended timeout (30s) to handle slow database connections, + * especially in development with network latency to remote PostgreSQL. */ async createOrder( input: CreateOrderInput, @@ -259,6 +262,10 @@ export class OrderProcessingService { ); return order; + }, { + // Extended timeout for slow database connections (remote PostgreSQL in dev) + maxWait: 10000, // Max wait to acquire connection (10s) + timeout: 30000, // Transaction timeout (30s) - handles slow queries }); } @@ -395,6 +402,9 @@ export class OrderProcessingService { where: { id: orderId }, data: updateData, }); + }, { + maxWait: 10000, + timeout: 30000, }); } @@ -606,7 +616,7 @@ export class OrderProcessingService { html: `

Thank you for your order!

Order Number: ${order.orderNumber}

-

Total: $${order.totalAmount.toFixed(2)}

+

Total: ৳${order.totalAmount.toFixed(2)}

We'll send you a shipping confirmation when your order ships.

`, }); diff --git a/src/lib/services/store.service.ts b/src/lib/services/store.service.ts index e2ec7ade..3a577347 100644 --- a/src/lib/services/store.service.ts +++ b/src/lib/services/store.service.ts @@ -22,9 +22,9 @@ export const CreateStoreSchema = z.object({ city: z.string().optional(), state: z.string().optional(), postalCode: z.string().optional(), - country: z.string().default('US'), - currency: z.string().default('USD'), - timezone: z.string().default('UTC'), + country: z.string().default('BD'), + currency: z.string().default('BDT'), + timezone: z.string().default('Asia/Dhaka'), locale: z.string().default('en'), subscriptionPlan: z.nativeEnum(SubscriptionPlan).default(SubscriptionPlan.FREE), organizationId: z.string().optional(), // Optional - will be derived from session if not provided diff --git a/src/lib/storefront/defaults.ts b/src/lib/storefront/defaults.ts index 26b35fd1..3aa8310d 100644 --- a/src/lib/storefront/defaults.ts +++ b/src/lib/storefront/defaults.ts @@ -38,7 +38,7 @@ export function getDefaultTrustBadges(): TrustBadge[] { enabled: true, icon: 'truck', title: 'Free Shipping', - description: 'On orders over $50', + description: 'On orders over ৳5,000', }, { id: createId(), diff --git a/src/lib/stores/cart-store.ts b/src/lib/stores/cart-store.ts index 1129fab9..010d985f 100644 --- a/src/lib/stores/cart-store.ts +++ b/src/lib/stores/cart-store.ts @@ -34,9 +34,8 @@ interface CartActions { clearCart: () => void; getItemCount: () => number; getSubtotal: () => number; - getEstimatedTax: (taxRate?: number) => number; getEstimatedShipping: (freeShippingThreshold?: number, shippingCost?: number) => number; - getTotal: (taxRate?: number, freeShippingThreshold?: number, shippingCost?: number) => number; + getTotal: (freeShippingThreshold?: number, shippingCost?: number) => number; } type CartStore = CartState & CartActions; @@ -204,10 +203,6 @@ export const useCart = create()( ); }, - getEstimatedTax: (taxRate = 0.1) => { - return get().getSubtotal() * taxRate; - }, - getEstimatedShipping: ( freeShippingThreshold = 50, shippingCost = 10 @@ -217,15 +212,13 @@ export const useCart = create()( }, getTotal: ( - taxRate = 0.1, freeShippingThreshold = 50, shippingCost = 10 ) => { // Cache subtotal to avoid redundant calculations const subtotal = get().getSubtotal(); - const tax = subtotal * taxRate; const shipping = subtotal >= freeShippingThreshold ? 0 : shippingCost; - return subtotal + tax + shipping; + return subtotal + shipping; }, }), { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d5179a05..634d039d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -8,32 +8,32 @@ export function cn(...inputs: ClassValue[]) { /** * Format a number as currency * @param amount - The amount to format - * @param currency - The currency code (default: USD) - * @param locale - The locale for formatting (default: en-US) + * @param currency - The currency code (default: BDT) + * @param locale - The locale for formatting (default: bn-BD) */ export function formatCurrency( amount: number, - currency: string = "USD", - locale: string = "en-US" + currency: string = "BDT", + locale: string = "bn-BD" ): string { return new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: 2, maximumFractionDigits: 2, - }).format(amount / 100); // Assuming amounts are stored in cents + }).format(amount / 100); // Assuming amounts are stored in paisa } /** - * Format a number as currency from dollars (not cents) - * @param amount - The amount in dollars - * @param currency - The currency code (default: USD) - * @param locale - The locale for formatting (default: en-US) + * Format a number as currency from taka (not paisa) + * @param amount - The amount in taka + * @param currency - The currency code (default: BDT) + * @param locale - The locale for formatting (default: bn-BD) */ -export function formatCurrencyFromDollars( +export function formatCurrencyFromTaka( amount: number, - currency: string = "USD", - locale: string = "en-US" + currency: string = "BDT", + locale: string = "bn-BD" ): string { return new Intl.NumberFormat(locale, { style: "currency", @@ -43,6 +43,9 @@ export function formatCurrencyFromDollars( }).format(amount); } +// Alias for backward compatibility +export const formatCurrencyFromDollars = formatCurrencyFromTaka; + /** * Get the first image URL from a product's image data * Handles both thumbnailUrl and JSON-encoded images array diff --git a/src/test/vitest.d.ts b/src/test/vitest.d.ts index 24271ea0..24eb1277 100644 --- a/src/test/vitest.d.ts +++ b/src/test/vitest.d.ts @@ -11,9 +11,9 @@ import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers' declare global { namespace Vi { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type + // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface Assertion extends TestingLibraryMatchers {} - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type + // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface AsymmetricMatchersContaining extends TestingLibraryMatchers {} } } diff --git a/test-results/.playwright-artifacts-0/562095743bb31e952bf1810e7b9c27c2.webm b/test-results/.playwright-artifacts-0/562095743bb31e952bf1810e7b9c27c2.webm new file mode 100644 index 00000000..93d28510 Binary files /dev/null and b/test-results/.playwright-artifacts-0/562095743bb31e952bf1810e7b9c27c2.webm differ diff --git a/test-results/.playwright-artifacts-0/626b241b95209db735a5ffa3865cd7f2.png b/test-results/.playwright-artifacts-0/626b241b95209db735a5ffa3865cd7f2.png new file mode 100644 index 00000000..d1df9a24 Binary files /dev/null and b/test-results/.playwright-artifacts-0/626b241b95209db735a5ffa3865cd7f2.png differ diff --git a/test-results/.playwright-artifacts-0/ba17c804a0ae05557a39a7b9e7c20c62.webm b/test-results/.playwright-artifacts-0/ba17c804a0ae05557a39a7b9e7c20c62.webm new file mode 100644 index 00000000..e69de29b diff --git a/test-results/.playwright-artifacts-0/e3f1263f77b0e8bc285fe178b2490e5b.png b/test-results/.playwright-artifacts-0/e3f1263f77b0e8bc285fe178b2490e5b.png new file mode 100644 index 00000000..9efe3a0f Binary files /dev/null and b/test-results/.playwright-artifacts-0/e3f1263f77b0e8bc285fe178b2490e5b.png differ diff --git a/test-results/.playwright-artifacts-0/f2e1db96258f35be322ec5f4b68da994.webm b/test-results/.playwright-artifacts-0/f2e1db96258f35be322ec5f4b68da994.webm new file mode 100644 index 00000000..d01e57da Binary files /dev/null and b/test-results/.playwright-artifacts-0/f2e1db96258f35be322ec5f4b68da994.webm differ diff --git a/test-results/.playwright-artifacts-93/53799fedc676500758b58c88b0a4d137.webm b/test-results/.playwright-artifacts-93/53799fedc676500758b58c88b0a4d137.webm new file mode 100644 index 00000000..b3501d1a Binary files /dev/null and b/test-results/.playwright-artifacts-93/53799fedc676500758b58c88b0a4d137.webm differ diff --git a/test-results/.playwright-artifacts-93/96507b703850bd86d86c0c90ea223ce7.png b/test-results/.playwright-artifacts-93/96507b703850bd86d86c0c90ea223ce7.png new file mode 100644 index 00000000..9a2d0c07 Binary files /dev/null and b/test-results/.playwright-artifacts-93/96507b703850bd86d86c0c90ea223ce7.png differ diff --git a/test-results/.playwright-artifacts-93/b77902f0d17ca3636c320006f3f1c6bc.webm b/test-results/.playwright-artifacts-93/b77902f0d17ca3636c320006f3f1c6bc.webm new file mode 100644 index 00000000..e69de29b diff --git a/test-results/.playwright-artifacts-94/27376374300dfd4c17ed0c876dbd8679.webm b/test-results/.playwright-artifacts-94/27376374300dfd4c17ed0c876dbd8679.webm new file mode 100644 index 00000000..e69de29b