From b01f0debc2ff3c06d4a94b5b6f336f263a56da09 Mon Sep 17 00:00:00 2001 From: liyaqath Date: Tue, 23 Jun 2026 11:04:34 +0530 Subject: [PATCH] frontend screens implementataions --- package-lock.json | 131 ++- package.json | 4 +- src/App.tsx | 8 +- .../components/ProtectedRoute.tsx | 4 +- src/authentication/pages/Login.tsx | 40 +- src/authentication/pages/SSOCallback.tsx | 35 +- src/authentication/services/authService.ts | 167 +--- src/authentication/types/auth.types.ts | 136 +-- src/components/customs/InfoCards.tsx | 2 +- src/components/customs/Table.tsx | 2 +- src/components/layouts/Headers.tsx | 25 +- src/components/layouts/MainLayout.tsx | 4 +- src/components/layouts/Sidebar.tsx | 11 - src/contexts/LanguageContext.tsx | 32 + .../asset-families/pages/AssetFamilyList.tsx | 287 +++++- .../asset-families/pages/NewAssetFamily.tsx | 156 ++- .../validation/asset-families.schema.ts | 12 +- .../asset-types/pages/AssetTypeList.tsx | 312 +++++- .../asset-types/pages/NewAssetType.tsx | 156 ++- .../validation/asset-types.schema.ts | 12 +- src/features/assets/pages/AssetList.tsx | 512 +++++++++- src/features/assets/pages/NewAsset.tsx | 144 ++- .../assets/validation/assets.schema.ts | 11 +- .../pages/AttributeGroupList.tsx | 179 +++- .../pages/NewAttributeGroup.tsx | 156 ++- .../validation/attribute-groups.schema.ts | 12 +- .../attributes/pages/AttributeGroupList.tsx | 151 +++ .../attributes/pages/AttributeList.tsx | 203 +++- .../attributes/pages/NewAttribute.tsx | 442 ++++----- .../attributes/routes/attribute.routes.tsx | 2 + .../attributes/validation/attribute.schema.ts | 46 +- src/features/brands/index.ts | 2 +- src/features/brands/pages/NewBrandForm.tsx | 290 +++--- src/features/categories/index.ts | 5 - .../categories/pages/CategoryList.tsx | 80 +- src/features/categories/pages/NewCategory.tsx | 307 +++--- .../categories/validation/category.schema.ts | 26 +- src/features/channels/pages/ChannelList.tsx | 170 +++- src/features/channels/pages/NewChannel.tsx | 354 ++++++- .../channels/validation/channels.schema.ts | 11 +- src/features/dashboard/pages/Dashboard.tsx | 54 +- src/features/family/pages/FamilyList.tsx | 2 +- src/features/family/pages/NewFamily.tsx | 552 ++++++----- .../family/validation/family.schema.ts | 27 +- .../imports/components/ImportCard.tsx | 201 ++++ .../components/SupplierPerformancePanel.tsx | 26 + .../components/UploadNewImportPanel.tsx | 64 ++ src/features/imports/data/mockImports.ts | 52 + src/features/imports/pages/ImportList.tsx | 149 ++- src/features/imports/pages/NewImport.tsx | 144 ++- src/features/imports/types/imports.types.ts | 17 + .../imports/validation/imports.schema.ts | 11 +- .../integrations/pages/IntegrationList.tsx | 206 +++- .../integrations/pages/NewIntegration.tsx | 624 ++++++++++-- .../validation/integrations.schema.ts | 11 +- src/features/product/pages/NewProduct.tsx | 932 +++++++++++++++--- .../product/validation/product.schema.ts | 22 +- src/features/reports/pages/NewReport.tsx | 144 ++- src/features/reports/pages/ReportList.tsx | 384 +++++++- .../reports/validation/reports.schema.ts | 11 +- src/features/scopes/pages/NewScope.tsx | 145 ++- src/features/scopes/pages/ScopeList.tsx | 13 +- .../scopes/validation/scopes.schema.ts | 11 +- src/features/settings/pages/NewSetting.tsx | 144 ++- src/features/settings/pages/SettingList.tsx | 413 +++++++- .../settings/validation/settings.schema.ts | 11 +- src/features/units/pages/NewUnit.tsx | 253 +++-- src/features/units/pages/UnitList.tsx | 4 +- src/features/units/validation/unit.schema.ts | 27 +- src/features/users/pages/NewUser.tsx | 144 ++- src/features/users/pages/UserList.tsx | 383 ++++++- src/features/users/validation/users.schema.ts | 11 +- src/features/variants/pages/NewVariant.tsx | 259 +++-- src/features/variants/pages/VariantList.tsx | 2 +- .../variants/validation/variant.schema.ts | 41 +- src/features/workflow/pages/NewWorkflow.tsx | 144 ++- src/features/workflow/pages/WorkflowList.tsx | 178 +++- .../workflow/validation/workflow.schema.ts | 11 +- 78 files changed, 7748 insertions(+), 2718 deletions(-) create mode 100644 src/contexts/LanguageContext.tsx create mode 100644 src/features/attributes/pages/AttributeGroupList.tsx create mode 100644 src/features/imports/components/ImportCard.tsx create mode 100644 src/features/imports/components/SupplierPerformancePanel.tsx create mode 100644 src/features/imports/components/UploadNewImportPanel.tsx create mode 100644 src/features/imports/data/mockImports.ts diff --git a/package-lock.json b/package-lock.json index bdad87e..942c30f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@tailwindcss/vite": "^4.3.1", "axios": "^1.18.0", "clsx": "^2.1.1", + "formik": "^2.4.9", "lucide-react": "^1.18.0", "react": "^19.2.6", "react-day-picker": "^10.0.1", @@ -26,7 +27,8 @@ "react-toastify": "^11.1.0", "recharts": "^3.8.1", "tailwind-merge": "^3.6.0", - "tailwindcss": "^4.3.1" + "tailwindcss": "^4.3.1", + "yup": "^1.7.1" }, "devDependencies": { "@babel/core": "^7.29.0", @@ -1714,6 +1716,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2446,6 +2460,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2899,6 +2922,31 @@ "node": ">= 6" } }, + "node_modules/formik": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.9.tgz", + "integrity": "sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3079,6 +3127,21 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -3513,6 +3576,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3758,6 +3833,12 @@ "node": ">= 0.8.0" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -3824,6 +3905,12 @@ "react": "^19.2.7" } }, + "node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", + "license": "MIT" + }, "node_modules/react-is": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", @@ -4123,12 +4210,24 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -4146,6 +4245,12 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -4178,6 +4283,18 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -4797,6 +4914,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index 12ff2db..ec733c4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@tailwindcss/vite": "^4.3.1", "axios": "^1.18.0", "clsx": "^2.1.1", + "formik": "^2.4.9", "lucide-react": "^1.18.0", "react": "^19.2.6", "react-day-picker": "^10.0.1", @@ -28,7 +29,8 @@ "react-toastify": "^11.1.0", "recharts": "^3.8.1", "tailwind-merge": "^3.6.0", - "tailwindcss": "^4.3.1" + "tailwindcss": "^4.3.1", + "yup": "^1.7.1" }, "devDependencies": { "@babel/core": "^7.29.0", diff --git a/src/App.tsx b/src/App.tsx index 2692563..d072e2c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,9 @@ -// src/App.tsx import { useEffect, useRef, useState } from 'react'; import AppRoutes from './routes/AppRoutes'; import { ToastContainer } from 'react-toastify'; +import { LanguageProvider } from './contexts/LanguageContext'; import { useAppDispatch } from './store'; -import { setPermissions } from './store/slices/authSlice'; + function App() { const dispatch = useAppDispatch(); @@ -35,10 +35,10 @@ function App() { } return ( - <> + - + ); } diff --git a/src/authentication/components/ProtectedRoute.tsx b/src/authentication/components/ProtectedRoute.tsx index 00f2e9e..afaf403 100644 --- a/src/authentication/components/ProtectedRoute.tsx +++ b/src/authentication/components/ProtectedRoute.tsx @@ -9,9 +9,9 @@ export const AuthGuard: React.FC<{ children: React.ReactNode }> = ({ children }) // ProtectedRoute for permissions (still useful) export const ProtectedRoute: React.FC<{ - node: string; + node?: string; children: React.ReactNode; -}> = ({ node, children }) => { +}> = ({ children }) => { // For now, allow everything in development // You can later connect real permission logic return <>{children}; diff --git a/src/authentication/pages/Login.tsx b/src/authentication/pages/Login.tsx index df4252e..44efc29 100644 --- a/src/authentication/pages/Login.tsx +++ b/src/authentication/pages/Login.tsx @@ -1,8 +1,7 @@ import { useState } from "react"; import { Navigate, useLocation, Link } from "react-router-dom"; import { useAppDispatch, useAppSelector } from "../../store"; -import { setCredentials, setLoading } from "../../store/auth.store"; -import { authService } from "../services/authService"; +import { setAuthLoading } from "../../store/slices/authSlice"; import { Package, Eye, EyeOff, Check } from "lucide-react"; import "./Login.css"; @@ -68,42 +67,17 @@ export const Login = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(""); - dispatch(setLoading(true)); + dispatch(setAuthLoading(true)); try { console.log("Attempting login..."); - const response = await authService.login({ email, password }); - console.log("Login response:", response); - - if (response.success) { - dispatch( - setCredentials({ - user: response.data.user, - accessToken: response.data.accessToken, - refreshToken: response.data.refreshToken, - permissions: response.data.permissions, - accessibleStores: response.data.accessibleStores, - isGlobalAccess: response.data.isGlobalAccess, - }), - ); - } else { - setError(response.message || "Login failed"); - dispatch(setLoading(false)); - } + // MOCK LOGIN + setTimeout(() => { + dispatch(setAuthLoading(false)); + }, 1000); } catch (err: unknown) { console.error("Login error:", err); - // Handle axios errors - if (err && typeof err === "object" && "response" in err) { - const axiosError = err as { - response?: { data?: { message?: string } }; - }; - setError(axiosError.response?.data?.message || "Login failed"); - } else if (err instanceof Error) { - setError(err.message); - } else { - setError("Login failed"); - } - dispatch(setLoading(false)); + dispatch(setAuthLoading(false)); } }; diff --git a/src/authentication/pages/SSOCallback.tsx b/src/authentication/pages/SSOCallback.tsx index 9b33eed..e5873bf 100644 --- a/src/authentication/pages/SSOCallback.tsx +++ b/src/authentication/pages/SSOCallback.tsx @@ -1,8 +1,7 @@ import { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAppDispatch } from '../../store'; -import { setCredentials, setLoading } from '../../store/auth.store'; -import { authService } from '../services/authService'; +import { setAuthLoading } from '../../store/slices/authSlice'; import { Package } from 'lucide-react'; import './Login.css'; @@ -34,34 +33,14 @@ export const SSOCallback = () => { return; } - // Exchange code for token - dispatch(setLoading(true)); try { - setStatus('Exchanging authorization code...'); - const response = await authService.exchangeSaaSToken(code); - - if (response.success && response.data) { - setStatus('Login successful! Redirecting...'); - - dispatch( - setCredentials({ - user: response.data.user, - accessToken: response.data.accessToken || (response.data as any).token, - refreshToken: response.data.refreshToken || (response.data as any).token, - permissions: response.data.permissions || [], - accessibleStores: response.data.accessibleStores, - isGlobalAccess: response.data.isGlobalAccess, - }) - ); - - // Redirect to dashboard after successful login - setTimeout(() => navigate('/dashboard'), 500); - } else { - throw new Error(response.message || 'SSO exchange failed'); - } + dispatch(setAuthLoading(true)); + // Mock exchange + setStatus('Login successful! Redirecting...'); + setTimeout(() => navigate('/dashboard'), 1500); } catch (err: unknown) { console.error('SSO Exchange error:', err); - + if (err && typeof err === 'object' && 'response' in err) { const axiosError = err as { response?: { data?: { message?: string } }; @@ -74,7 +53,7 @@ export const SSOCallback = () => { } setStatus('SSO login failed'); - dispatch(setLoading(false)); + dispatch(setAuthLoading(false)); setTimeout(() => navigate('/login'), 3000); } }; diff --git a/src/authentication/services/authService.ts b/src/authentication/services/authService.ts index 1db7511..7cd906f 100644 --- a/src/authentication/services/authService.ts +++ b/src/authentication/services/authService.ts @@ -1,152 +1,15 @@ -// import { axiosInstance, tokenService } from '../../services'; -// import { API_ENDPOINTS } from '../../utils/constants'; -// import type { LoginRequest, LoginResponse } from '../types'; - -// /** -// * Auth service with real API calls -// * Uses centralized TokenService for token management -// */ -// export const authService = { -// // Login - calls backend /auth/login endpoint and stores tokens -// login: async (credentials: LoginRequest): Promise => { -// const response = await axiosInstance.post(API_ENDPOINTS.AUTH.LOGIN, credentials); -// const data = response.data as LoginResponse; - -// // Store tokens using TokenService -// if (data.data?.accessToken) { -// tokenService.setTokens(data.data.accessToken, data.data.refreshToken); -// } - -// // Store user data in tokenService for backup -// if (data.data?.user) { -// tokenService.setUserData({ -// id: data.data.user.id, -// email: data.data.user.email, -// name: data.data.user.name ?? undefined, -// company_id: data.data.user.company?.id, -// }); -// } - -// return data; -// }, - -// // Refresh access token using refresh token -// refreshToken: async (): Promise<{ accessToken: string } | null> => { -// const refreshToken = tokenService.getRefreshToken(); - -// if (!refreshToken) { -// return null; -// } - -// try { -// const response = await axiosInstance.post('/api/v1/auth/refresh-token', { -// refresh_token: refreshToken, -// }); - -// const { accessToken } = response.data.data; - -// // Update stored access token -// tokenService.setTokens(accessToken, refreshToken); - -// return { accessToken }; -// } catch (error) { -// // Refresh failed - clear all tokens -// tokenService.clearAll(); -// return null; -// } -// }, - -// // Logout - calls backend /auth/logout endpoint and clears tokens -// logout: async (): Promise => { -// try { -// await axiosInstance.post(API_ENDPOINTS.AUTH.LOGOUT); -// } finally { -// // Always clear tokens on logout, even if API call fails -// tokenService.clearAll(); -// } -// }, - -// // Forgot password - sends OTP to email -// forgotPassword: async (email: string): Promise<{ message: string }> => { -// const response = await axiosInstance.post('/api/v1/auth/forgot-password', { email }); -// return response.data; -// }, - -// // Verify OTP -// verifyOTP: async (email: string, otp: string): Promise<{ message: string }> => { -// const response = await axiosInstance.post('/api/v1/auth/verify-otp', { email, otp }); -// return response.data; -// }, - -// // Reset password with OTP -// resetPassword: async (email: string, otp: string, newPassword: string, confirmPassword: string): Promise<{ message: string }> => { -// const response = await axiosInstance.post('/api/v1/auth/reset-password', { -// email, -// otp, -// new_password: newPassword, -// confirm_password: confirmPassword, -// }); -// return response.data; -// }, - -// // Change password (authenticated) -// changePassword: async (oldPassword: string, newPassword: string, confirmPassword: string): Promise<{ message: string }> => { -// const response = await axiosInstance.post('/api/v1/auth/change-password', { -// old_password: oldPassword, -// new_password: newPassword, -// confirm_password: confirmPassword, -// }); -// return response.data; -// }, - -// // Check if user is authenticated -// isAuthenticated: (): boolean => { -// return tokenService.isAuthenticated(); -// }, - -// // Get current user data -// getCurrentUser: () => { -// return tokenService.getUserData(); -// }, - -// // Check if token is expired -// isTokenExpired: (): boolean => { -// return tokenService.isTokenExpired(); -// }, - -// // Check if we have a refresh token -// hasRefreshToken: (): boolean => { -// return !!tokenService.getRefreshToken(); -// }, - -// // Exchange SaaS authorization code for token (SSO) -// exchangeSaaSToken: async (code: string): Promise => { -// // Corrected path to match app.js mounting: /api/v1/sso/exchange -// const response = await axiosInstance.post('/api/v1/sso/exchange', { code }); -// const data = response.data as LoginResponse; - -// // Store tokens using TokenService -// if (data.data?.accessToken || (data.data as any)?.token) { -// const token = data.data?.accessToken || (data.data as any)?.token; -// tokenService.setTokens(token, token); -// } - -// // Store user data -// if (data.data?.user) { -// tokenService.setUserData({ -// id: data.data.user.id, -// email: data.data.user.email, -// name: data.data.user.name ?? undefined, -// company_id: data.data.user.company?.id, -// }); -// } - -// return data; -// }, - -// // Get current user profile from backend -// getMe: async (): Promise => { -// const response = await axiosInstance.get('/api/v1/auth/me'); -// return response.data; -// }, -// }; +export const authService = { + login: async (_credentials: any) => ({ data: {} }), + refreshToken: async () => null, + logout: async () => {}, + forgotPassword: async (_email: string) => ({ message: "OTP sent" }), + verifyOTP: async (_email: string, _otp: string) => ({ message: "OTP verified" }), + resetPassword: async (_email: string, _otp: string, _newP: string, _confP: string) => ({ message: "Password reset" }), + changePassword: async (_oldP: string, _newP: string, _confP: string) => ({ message: "Password changed" }), + isAuthenticated: () => false, + getCurrentUser: () => null, + isTokenExpired: () => false, + hasRefreshToken: () => false, + exchangeSaaSToken: async (_code: string) => ({ data: {} }), + getMe: async () => ({}) +}; diff --git a/src/authentication/types/auth.types.ts b/src/authentication/types/auth.types.ts index 603bdaa..7db01d5 100644 --- a/src/authentication/types/auth.types.ts +++ b/src/authentication/types/auth.types.ts @@ -1,74 +1,74 @@ -// export interface Store { -// id: number | string; -// store_code: string; -// store_name: string; -// store_type: string; -// } +export interface Store { + id: number | string; + store_code: string; + store_name: string; + store_type: string; +} -// // User information from backend -// export interface User { -// id: number; -// name: string | null; -// email: string; -// type: string; -// company_id: number | null; -// company: { -// id: number; -// company_name: string; -// } | null; -// roles?: { -// id: number; -// name: string; -// code: string; -// }[]; -// stores: Store[]; -// store_id?: number | string; -// } +// User information from backend +export interface User { + id: number; + name: string | null; + email: string; + type: string; + company_id: number | null; + company: { + id: number; + company_name: string; + } | null; + roles?: { + id: number; + name: string; + code: string; + }[]; + stores: Store[]; + store_id?: number | string; +} -// // Permission actions available for each module -// export interface PermissionActions { -// view?: boolean; -// create?: boolean; -// edit?: boolean; -// delete?: boolean; -// alter?: boolean; -// import?: boolean; -// export?: boolean; -// } +// Permission actions available for each module +export interface PermissionActions { + view?: boolean; + create?: boolean; + edit?: boolean; + delete?: boolean; + alter?: boolean; + import?: boolean; + export?: boolean; +} -// // Permissions structure: nested object from backend -// export type Permissions = Record; +// Permissions structure: nested object from backend +export type Permissions = Record; -// // Authentication state in Redux store -// export interface AuthState { -// user: User | null; -// accessToken: string | null; -// permissions: Permissions; -// isAuthenticated: boolean; -// isLoading: boolean; -// currentStore: Store | null; -// accessibleStores: Store[]; -// isGlobalAccess: boolean; -// } +// Authentication state in Redux store +export interface AuthState { + user: User | null; + accessToken: string | null; + permissions: Permissions; + isAuthenticated: boolean; + isLoading: boolean; + currentStore: Store | null; + accessibleStores: Store[]; + isGlobalAccess: boolean; +} -// // API response from login endpoint -// export interface LoginResponse { -// StatusCode: number; -// data: { -// token: any; -// user: User; -// accessToken: string; -// refreshToken?: string; -// permissions: Permissions; -// accessibleStores?: Store[]; -// isGlobalAccess?: boolean; -// }; -// message: string; -// success: boolean; -// } +// API response from login endpoint +export interface LoginResponse { + StatusCode: number; + data: { + token: any; + user: User; + accessToken: string; + refreshToken?: string; + permissions: Permissions; + accessibleStores?: Store[]; + isGlobalAccess?: boolean; + }; + message: string; + success: boolean; +} -// // Login request payload -// export interface LoginRequest { -// email: string; -// password: string; -// } +// Login request payload +export interface LoginRequest { + email: string; + password: string; +} diff --git a/src/components/customs/InfoCards.tsx b/src/components/customs/InfoCards.tsx index efd650c..e8a3cc8 100644 --- a/src/components/customs/InfoCards.tsx +++ b/src/components/customs/InfoCards.tsx @@ -14,7 +14,7 @@ export interface InfoCardProps { icon: ReactNode; trend?: string; trendDirection?: "up" | "down" | "neutral"; - subtitle?: string; + subtitle?: ReactNode; variant?: InfoCardVariant; className?: string; } diff --git a/src/components/customs/Table.tsx b/src/components/customs/Table.tsx index e5a88d2..572b1b2 100644 --- a/src/components/customs/Table.tsx +++ b/src/components/customs/Table.tsx @@ -1,5 +1,5 @@ import { type ReactNode, useState, useMemo } from "react"; -import { ChevronUp, ChevronDown, MoreVertical } from "lucide-react"; +import { ChevronUp, ChevronDown } from "lucide-react"; import { Button } from "./Button"; export interface TableColumn { diff --git a/src/components/layouts/Headers.tsx b/src/components/layouts/Headers.tsx index a42ce64..7d8710f 100644 --- a/src/components/layouts/Headers.tsx +++ b/src/components/layouts/Headers.tsx @@ -1,16 +1,20 @@ // src/components/layouts/Headers.tsx -import React, { useState } from "react"; import { useLocation } from "react-router-dom"; -import { Bell, ChevronDown, Building2 } from "lucide-react"; +import { Bell, ChevronDown, Building2, Globe } from "lucide-react"; +import { useLanguage, type Language } from "../../contexts/LanguageContext"; const PAGE_TITLES: Record = { "/dashboard": { title: "Dashboard", subtitle: "Overview of your PIM platform" }, "/products": { title: "Products", subtitle: "Manage your product catalog" }, "/families": { title: "Product Families", subtitle: "Manage product families" }, + "/reports": { title: "Reports & Analytics", subtitle: "Comprehensive insights into your product catalog performance" }, + "/users": { title: "Users & Roles", subtitle: "Manage team members, permissions, and access controls" }, + "/settings": { title: "Settings", subtitle: "System configurations and preferences" }, }; export function Header() { const location = useLocation(); + const { language, setLanguage } = useLanguage(); const page = PAGE_TITLES[location.pathname] || { title: "PIM Platform" }; @@ -29,6 +33,23 @@ export function Header() { {/* Right Side Controls */}
+
+ + +
+
+ const filtered = MOCK_FAMILIES.filter(f => + f.name.toLowerCase().includes(searchQuery.toLowerCase()) ); return ( -
+ {/* Header */} +
-

AssetFamilys

-

Manage your asset families

+ +

Asset Families

+

Configure asset types, attributes, and transformations for your digital asset library

-
-
- + +
+ {/* Left Sidebar */} +
+
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-4 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500" + /> +
+
+
+ {filtered.map((family) => { + const Icon = family.icon; + const isSelected = selectedFamily.id === family.id; + return ( +
setSelectedFamily(family)} + className={`p-3 rounded-xl mb-1.5 cursor-pointer transition-all ${ + isSelected ? "bg-purple-50 border border-purple-200 ring-1 ring-purple-100" : "border border-transparent hover:bg-gray-50 hover:border-gray-200" + }`} + > +
+
+ +
+
+
+ {family.name} +
+

{family.description}

+
+ + {family.code} + + {family.assets} assets +
+
+
+
+ ); + })} +
+
+ + {/* Right Detail Panel */} +
+ {/* Family Header Card */} +
+
+
+
+ +
+
+

{selectedFamily.name}

+

{selectedFamily.description}

+
+
+ Code: + + {selectedFamily.code} + +
+
+ Type: + + {selectedFamily.type} + +
+
+ Assets: + {selectedFamily.assets} +
+
+
+
+ +
+
+ + {/* Asset Attributes Card */} +
+
+
+ +

Asset Attributes

+ Metadata fields required for assets in this family +
+
+ {selectedFamily.attributes.length} attributes + +
+
+ + {attributesOpen && ( +
+
+
Attribute Code
+
Label
+
Type
+
Required
+
Options
+
+
+ {selectedFamily.attributes.map((attr, i) => ( +
+
+ + {attr.code} + +
+
{attr.label}
+
{attr.type}
+
+ {attr.required ? ( + + Required + + ) : ( + Optional + )} +
+
{attr.options || "—"}
+
+ ))} +
+
+ +
+
+ )} +
+
); diff --git a/src/features/asset-families/pages/NewAssetFamily.tsx b/src/features/asset-families/pages/NewAssetFamily.tsx index 34158da..1172553 100644 --- a/src/features/asset-families/pages/NewAssetFamily.tsx +++ b/src/features/asset-families/pages/NewAssetFamily.tsx @@ -1,69 +1,131 @@ -import { useState, useEffect } from "react"; +import { useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { useFormik } from "formik"; +import { ArrowLeft, Save } from "lucide-react"; import { PageWrapper } from "../../../components/layouts/PageWrapper"; import { Button } from "../../../components/customs/Button"; import { useAssetFamily } from "../hook/useAssetFamily"; -import { validateAssetFamily } from "../validation/asset-families.schema"; -import { assetFamiliesService } from "../services/asset-families.service"; +import { assetFamilySchema } from "../validation/asset-families.schema"; import type { AssetFamilyCreateRequest } from "../types/asset-families.types"; +const STATUS_OPTIONS = [ + { value: 'active', label: 'Active' }, + { value: 'inactive', label: 'Inactive' }, +]; + +const inputClass = (error?: boolean) => + `w-full border ${error ? 'border-red-500 focus:ring-red-500' : 'border-gray-200 focus:ring-purple-500'} rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:border-transparent transition-shadow bg-white`; +const errorClass = 'text-xs text-red-500 mt-1'; +const labelClass = 'block text-sm font-medium text-gray-700 mb-1.5'; + export default function NewAssetFamily() { const navigate = useNavigate(); - const { id } = useParams<{ id: string }>(); + const { id } = useParams<{ id?: string }>(); const isEdit = Boolean(id); - const { createItem, updateItem } = useAssetFamily(); - const [form, setForm] = useState>({ name: "", status: "active" }); - const [errors, setErrors] = useState>({}); - const [saving, setSaving] = useState(false); + const { createItem, updateItem, items, fetchItems } = useAssetFamily(); useEffect(() => { - if (isEdit && id) { - assetFamiliesService.getById(id).then(item => { - if (item) setForm({ name: item.name, status: item.status }); - }); - } - }, [isEdit, id]); + fetchItems(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const validationErrors = validateAssetFamily(form); - if (Object.keys(validationErrors).length > 0) { - setErrors(validationErrors); - return; - } - setSaving(true); - try { - if (isEdit && id) { - await updateItem(id, form as any); - } else { - await createItem(form as AssetFamilyCreateRequest); + const formik = useFormik({ + initialValues: { + name: "", + status: "active" as "active" | "inactive", + }, + validationSchema: assetFamilySchema, + onSubmit: async (values, { setSubmitting }) => { + try { + if (isEdit && id) { + await updateItem(id, values as any); + } else { + await createItem(values as AssetFamilyCreateRequest); + } + navigate(".."); + } catch { + // Handled by service/hook + } finally { + setSubmitting(false); + } + }, + }); + + useEffect(() => { + if (isEdit && id && items.length > 0) { + const match = items.find((item) => item.id === id); + if (match) { + formik.setValues({ + name: match.name, + status: match.status as "active" | "inactive", + }); } - navigate(".."); - } catch { - // toast handled - } finally { - setSaving(false); } - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEdit, id, items]); return ( -
-

{isEdit ? 'Edit' : 'Create'} AssetFamily

-
-
- - setForm({ ...form, name: e.target.value })} - className="w-full border border-gray-300 rounded-lg px-3 py-2" /> - {errors.name &&

{errors.name}

} -
-
- - -
- + {/* Header */} +
+ +
+

{isEdit ? 'Edit Asset Family' : 'Create Asset Family'}

+

{isEdit ? 'Update asset family details' : 'Add a new asset family'}

+
+ +
+
+

Information

+
+
+ + + {formik.touched.name && formik.errors.name &&

{formik.errors.name}

} +
+ +
+ + +
+
+
+ + {/* Actions */} +
+ + +
+ ); } diff --git a/src/features/asset-families/validation/asset-families.schema.ts b/src/features/asset-families/validation/asset-families.schema.ts index ca71c37..3e3a4b5 100644 --- a/src/features/asset-families/validation/asset-families.schema.ts +++ b/src/features/asset-families/validation/asset-families.schema.ts @@ -1,6 +1,6 @@ -import type { AssetFamilyCreateRequest } from '../types/asset-families.types'; -export const validateAssetFamily = (data: Partial): Record => { - const errors: Record = {}; - if (!data.name?.trim()) errors.name = 'AssetFamily name is required'; - return errors; -}; +import * as Yup from 'yup'; + +export const assetFamilySchema = Yup.object().shape({ + name: Yup.string().required('Asset Family name is required'), + status: Yup.string().oneOf(['active', 'inactive']), +}); diff --git a/src/features/asset-types/pages/AssetTypeList.tsx b/src/features/asset-types/pages/AssetTypeList.tsx index 4d88ff3..ffc94b1 100644 --- a/src/features/asset-types/pages/AssetTypeList.tsx +++ b/src/features/asset-types/pages/AssetTypeList.tsx @@ -1,55 +1,285 @@ -import { useState, useEffect } from "react"; -import { Plus, Edit2, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { Plus, Edit2, Trash2, Eye, Search, Filter, Image as ImageIcon, Video, FileText, CheckCircle2, Layers, RefreshCw, LayoutGrid, BarChart2 } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { PageWrapper } from "../../../components/layouts/PageWrapper"; import { Button } from "../../../components/customs/Button"; -import { Table } from "../../../components/customs/Table"; -import { useAssetType } from "../hook/useAssetType"; -import type { AssetType } from "../types/asset-types.types"; + +// Mock Data +const MOCK_ASSET_TYPES = [ + { + id: "1", + name: "Primary Image", + description: "Main hero image displayed as the primary product photo on all...", + required: true, + code: "primary_image", + category: "Image", + categoryColor: "text-blue-600 bg-blue-50 border-blue-200", + icon: ImageIcon, + fileTypes: ["JPG", "JPEG", "PNG", "+1"], + status: "Active", + families: 48, + products: "12,840", + updatedDate: "2025-03-15", + updatedBy: "Admin User" + }, + { + id: "2", + name: "Gallery Image", + description: "Additional product images for gallery carousel and zoom...", + required: false, + code: "gallery_image", + category: "Image", + categoryColor: "text-blue-600 bg-blue-50 border-blue-200", + icon: ImageIcon, + fileTypes: ["JPG", "JPEG", "PNG", "+1"], + status: "Active", + families: 42, + products: "10,240", + updatedDate: "2025-03-12", + updatedBy: "Sarah Chen" + }, + { + id: "3", + name: "Thumbnail", + description: "Small optimized image used in product listings, search results and...", + required: false, + code: "thumbnail", + category: "Image", + categoryColor: "text-blue-600 bg-blue-50 border-blue-200", + icon: ImageIcon, + fileTypes: ["JPG", "JPEG", "PNG", "+1"], + status: "Active", + families: 38, + products: "9,820", + updatedDate: "2025-02-28", + updatedBy: "Michael Torres" + }, + { + id: "4", + name: "360° Image", + description: "Sequence of images enabling interactive 360-degree product...", + required: false, + code: "image_360", + category: "Image", + categoryColor: "text-blue-600 bg-blue-50 border-blue-200", + icon: ImageIcon, + fileTypes: ["JPG", "JPEG", "PNG", "+1"], + status: "Active", + families: 14, + products: "2,430", + updatedDate: "2025-03-08", + updatedBy: "Emma Wilson" + }, + { + id: "5", + name: "Product Video", + description: "Demonstration or promotional video showcasing product features...", + required: false, + code: "product_video", + category: "Video", + categoryColor: "text-red-600 bg-red-50 border-red-200", + icon: Video, + fileTypes: ["MP4", "MOV", "WEBM"], + status: "Active", + families: 22, + products: "4,310", + updatedDate: "2025-03-10", + updatedBy: "James Park" + }, + { + id: "6", + name: "Datasheet PDF", + description: "Detailed technical specifications, safety information, and...", + required: false, + code: "datasheet_pdf", + category: "Document", + categoryColor: "text-amber-600 bg-amber-50 border-amber-200", + icon: FileText, + fileTypes: ["PDF"], + status: "Active", + families: 30, + products: "7,840", + updatedDate: "2025-03-05", + updatedBy: "Admin User" + } +]; export default function AssetTypeList() { const navigate = useNavigate(); - const { items, fetchItems, deleteItem } = useAssetType(); - const [selected, setSelected] = useState>(new Set()); - - useEffect(() => { - fetchItems(); - }, [fetchItems]); - - const handleSelect = (ids: Set) => setSelected(ids); - - const handleDelete = async (item: AssetType) => { - if (confirm('Delete this item?')) { - await deleteItem(item.id); - } - }; - - const columns = [ - { key: "name", header: "Name", sortable: true }, - { key: "status", header: "Status", sortable: true }, - { key: "createdAt", header: "Created At", sortable: true, render: (val: string) => new Date(val).toLocaleDateString() }, - ]; - - const actions = (item: AssetType) => ( -
-
- ); + const [searchQuery, setSearchQuery] = useState(""); return ( -
-
-

AssetTypes

-

Manage your asset types

+ {/* Header */} +
+
+
+ +
+
+

Asset Types

+

Define reusable asset classifications for product families and products

+
+
+
+ +
-
-
-
+ + {/* KPI Cards */} +
+
+
+ +

Total Asset Types

+
+
12
+
across all categories
+
+
+
+ +

Active Types

+
+
11
+
1 inactive
+
+
+
+ +

Families Using

+
+
286
+
family assignments
+
+
+
+ +

Products Using

+
+
68,420
+
product associations
+
+
+ + {/* Toolbar */} +
+
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-9 pr-4 py-2 bg-white border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500" + /> +
+
+ + All Categories +
+
+ All Statuses +
+
+
+ 12 of 12 types +
+
+ + {/* Table */} +
+
+ + + + + + + + + + + + + + + {MOCK_ASSET_TYPES.map((type) => { + const Icon = type.icon; + return ( + + + + + + + + + + + + ); + })} + +
Asset Type NameCodeCategoryFile TypesStatusFamiliesProductsUpdatedActions
+
+
+ +
+
+
+ {type.name} + {type.required && ( + Required + )} +
+

{type.description}

+
+
+
+ + {type.code} + + + + + {type.category} + + +
+ {type.fileTypes.map((ft, i) => ( + + {ft}{i < type.fileTypes.length - 1 && ' '} + + ))} +
+
+
+
+ {type.status} +
+
+ {type.families} + + {type.products} + +
{type.updatedDate}
+
{type.updatedBy}
+
+
+ + + +
+
); diff --git a/src/features/asset-types/pages/NewAssetType.tsx b/src/features/asset-types/pages/NewAssetType.tsx index 12c284b..f209ebb 100644 --- a/src/features/asset-types/pages/NewAssetType.tsx +++ b/src/features/asset-types/pages/NewAssetType.tsx @@ -1,69 +1,131 @@ -import { useState, useEffect } from "react"; +import { useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { useFormik } from "formik"; +import { ArrowLeft, Save } from "lucide-react"; import { PageWrapper } from "../../../components/layouts/PageWrapper"; import { Button } from "../../../components/customs/Button"; import { useAssetType } from "../hook/useAssetType"; -import { validateAssetType } from "../validation/asset-types.schema"; -import { assetTypesService } from "../services/asset-types.service"; +import { assetTypeSchema } from "../validation/asset-types.schema"; import type { AssetTypeCreateRequest } from "../types/asset-types.types"; +const STATUS_OPTIONS = [ + { value: 'active', label: 'Active' }, + { value: 'inactive', label: 'Inactive' }, +]; + +const inputClass = (error?: boolean) => + `w-full border ${error ? 'border-red-500 focus:ring-red-500' : 'border-gray-200 focus:ring-purple-500'} rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:border-transparent transition-shadow bg-white`; +const errorClass = 'text-xs text-red-500 mt-1'; +const labelClass = 'block text-sm font-medium text-gray-700 mb-1.5'; + export default function NewAssetType() { const navigate = useNavigate(); - const { id } = useParams<{ id: string }>(); + const { id } = useParams<{ id?: string }>(); const isEdit = Boolean(id); - const { createItem, updateItem } = useAssetType(); - const [form, setForm] = useState>({ name: "", status: "active" }); - const [errors, setErrors] = useState>({}); - const [saving, setSaving] = useState(false); + const { createItem, updateItem, items, fetchItems } = useAssetType(); useEffect(() => { - if (isEdit && id) { - assetTypesService.getById(id).then(item => { - if (item) setForm({ name: item.name, status: item.status }); - }); - } - }, [isEdit, id]); + fetchItems(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const validationErrors = validateAssetType(form); - if (Object.keys(validationErrors).length > 0) { - setErrors(validationErrors); - return; - } - setSaving(true); - try { - if (isEdit && id) { - await updateItem(id, form as any); - } else { - await createItem(form as AssetTypeCreateRequest); + const formik = useFormik({ + initialValues: { + name: "", + status: "active" as "active" | "inactive", + }, + validationSchema: assetTypeSchema, + onSubmit: async (values, { setSubmitting }) => { + try { + if (isEdit && id) { + await updateItem(id, values as any); + } else { + await createItem(values as AssetTypeCreateRequest); + } + navigate(".."); + } catch { + // Handled by service/hook + } finally { + setSubmitting(false); + } + }, + }); + + useEffect(() => { + if (isEdit && id && items.length > 0) { + const match = items.find((item) => item.id === id); + if (match) { + formik.setValues({ + name: match.name, + status: match.status as "active" | "inactive", + }); } - navigate(".."); - } catch { - // toast handled - } finally { - setSaving(false); } - }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEdit, id, items]); return ( -
-

{isEdit ? 'Edit' : 'Create'} AssetType

-
-
- - setForm({ ...form, name: e.target.value })} - className="w-full border border-gray-300 rounded-lg px-3 py-2" /> - {errors.name &&

{errors.name}

} -
-
- - -
-
+ {/* Header */} +
+ +
+

{isEdit ? 'Edit Asset Type' : 'Create Asset Type'}

+

{isEdit ? 'Update asset type details' : 'Add a new asset type'}

+
+ +
+
+

Information

+
+
+ + + {formik.touched.name && formik.errors.name &&

{formik.errors.name}

} +
+ +
+ + +
+
+
+ + {/* Actions */} +
+ + +
+
); } diff --git a/src/features/asset-types/validation/asset-types.schema.ts b/src/features/asset-types/validation/asset-types.schema.ts index fa7b719..279311a 100644 --- a/src/features/asset-types/validation/asset-types.schema.ts +++ b/src/features/asset-types/validation/asset-types.schema.ts @@ -1,6 +1,6 @@ -import type { AssetTypeCreateRequest } from '../types/asset-types.types'; -export const validateAssetType = (data: Partial): Record => { - const errors: Record = {}; - if (!data.name?.trim()) errors.name = 'AssetType name is required'; - return errors; -}; +import * as Yup from 'yup'; + +export const assetTypeSchema = Yup.object().shape({ + name: Yup.string().required('Asset Type name is required'), + status: Yup.string().oneOf(['active', 'inactive']), +}); diff --git a/src/features/assets/pages/AssetList.tsx b/src/features/assets/pages/AssetList.tsx index 8f3252a..1541b5e 100644 --- a/src/features/assets/pages/AssetList.tsx +++ b/src/features/assets/pages/AssetList.tsx @@ -1,56 +1,474 @@ -import { useState, useEffect } from "react"; -import { Plus, Edit2, Trash2 } from "lucide-react"; -import { useNavigate } from "react-router-dom"; -import { PageWrapper } from "../../../components/layouts/PageWrapper"; +import { useState } from "react"; +import { + Search, ChevronDown, Download, Archive, Upload, + Image as ImageIcon, Video, FileText, Eye, Edit2, List, Grid, + FileBadge, BookOpen, Presentation, PenTool, Package, Layers, + ChevronRight +} from "lucide-react"; import { Button } from "../../../components/customs/Button"; -import { Table } from "../../../components/customs/Table"; -import { useAsset } from "../hook/useAsset"; -import type { Asset } from "../types/assets.types"; + +// Mock Data +const ASSET_CATEGORIES = [ + { id: "all", name: "All Assets", count: 125, icon: Layers, active: true }, + { id: "product-images", name: "Product Images", count: 45, icon: ImageIcon }, + { id: "variant-images", name: "Variant Images", count: 32, icon: Package }, + { id: "videos", name: "Videos", count: 8, icon: Video }, + { id: "documents", name: "Documents", count: 15, icon: FileText }, + { id: "certificates", name: "Certificates", count: 12, icon: FileBadge }, + { id: "manuals", name: "User Manuals", count: 6, icon: BookOpen }, + { id: "marketing", name: "Marketing Assets", count: 18, icon: Presentation }, + { id: "drawings", name: "Technical Drawings", count: 4, icon: PenTool }, + { id: "packaging", name: "Packaging Assets", count: 10, icon: Package }, +]; + +const MOCK_ASSETS = [ + { + id: "1", + name: "Nike Air Max Front View", + ext: "JPG", + type: "Image", + typeColor: "text-blue-600 bg-blue-50 border-blue-200", + iconColor: "text-blue-500", + product: "Nike Air Max", + productYear: "2024", + size: "2.4 MB", + resolution: "4000x3000", + tags: ["front", "product", "+1"], + status: "Active", + }, + { + id: "2", + name: "Nike Air Max Back View", + ext: "JPG", + type: "Image", + typeColor: "text-blue-600 bg-blue-50 border-blue-200", + iconColor: "text-blue-500", + product: "Nike Air Max", + productYear: "2024", + size: "2.1 MB", + resolution: "4000x3000", + tags: ["back", "product", "+1"], + status: "Active", + }, + { + id: "3", + name: "Product Demo Video", + ext: "MP4", + type: "Video", + typeColor: "text-purple-600 bg-purple-50 border-purple-200", + iconColor: "text-purple-500", + product: "Nike Air Max", + productYear: "2024", + size: "45.2 MB", + resolution: "1920x1080", + tags: ["demo", "marketing", "+1"], + status: "Active", + }, + { + id: "4", + name: "Product Manual", + ext: "PDF", + type: "Manual", + typeColor: "text-blue-600 bg-blue-50 border-blue-200", + iconColor: "text-gray-500", + product: "Nike Air Max", + productYear: "2024", + size: "3.5 MB", + resolution: "-", + tags: ["manual", "documentation"], + status: "Active", + }, +]; + +const MOCK_VARIANT_GROUPS = [ + { + id: "v1", + product: "Nike Air Max 2024", + variantsCount: 4, + variants: [ + { name: "Black / 8", assets: 3 }, + { name: "Black / 9", assets: 3 }, + { name: "White / 8", assets: 3 }, + { name: "White / 9", assets: 3 }, + ] + }, + { + id: "v2", + product: "MacBook Pro M4", + variantsCount: 4, + variants: [ + { name: "Black / 8", assets: 3 }, + { name: "Black / 9", assets: 3 }, + { name: "White / 8", assets: 3 }, + { name: "White / 9", assets: 3 }, + ] + }, + { + id: "v3", + product: "Samsung QLED TV", + variantsCount: 4, + variants: [ + { name: "Black / 8", assets: 3 }, + { name: "Black / 9", assets: 3 }, + { name: "White / 8", assets: 3 }, + { name: "White / 9", assets: 3 }, + ] + } +]; export default function AssetList() { - const navigate = useNavigate(); - const { items, fetchItems, deleteItem } = useAsset(); - const [selected, setSelected] = useState>(new Set()); - - useEffect(() => { - fetchItems(); - }, [fetchItems]); - - const handleSelect = (ids: Set) => setSelected(ids); - - const handleDelete = async (item: Asset) => { - if (confirm('Delete this item?')) { - await deleteItem(item.id); - } + const [activeTab, setActiveTab] = useState("Product Assets"); + + const getAssetIcon = (type: string, color: string) => { + if (type === "Image") return ; + if (type === "Video") return