diff --git a/.builderrules b/.builderrules index dc9ab16..69b67f5 100644 --- a/.builderrules +++ b/.builderrules @@ -1 +1,325 @@ -Make sure when generating with MUI components look at the examples in src/* for correct usage of this code on the specific MUI versions we use and patterns we want to replicate. \ No newline at end of file +Make sure when generating with MUI components look at the examples in src/* for correct usage of this code on the specific MUI versions we use and patterns we want to replicate. + +When doing any thing with user data, use this internal API of ours: + +# Users API + +A RESTful API for managing user data, built with Cloudflare Workers and D1 database. + +## Base URL + +``` +https://user-api.builder-io.workers.dev/api +``` + +## Authentication + +No authentication required for public endpoints. + +## Endpoints + +### List Users + +Returns a paginated list of users with optional search and sorting capabilities. + +``` +GET /users +``` + +#### Query Parameters + +| Parameter | Type | Default | Description | +| --------- | ------- | ------------ | -------------------------------------------------------- | +| `page` | integer | 1 | Page number for pagination | +| `perPage` | integer | 10 | Number of results per page | +| `search` | string | - | Search users by first name, last name, email, or city | +| `sortBy` | string | "first_name" | Field to sort results by | +| `span` | string | "week" | Time span view ("week" or "month") - affects page offset | + +#### Supported Sort Fields + +- `name.first` - Sort by first name +- `name.last` - Sort by last name +- `location.city` - Sort by city +- `location.country` - Sort by country +- `dob.age` - Sort by age +- `registered.date` - Sort by registration date + +#### Example Request + +```bash +curl "https://user-api.builder-io.workers.dev/api/users?page=1&perPage=20&search=john&sortBy=name.first" +``` + +#### Example Response + +```json +{ + "page": 1, + "perPage": 20, + "total": 500, + "span": "week", + "effectivePage": 1, + "data": [ + { + "login": { + "uuid": "test-uuid-1", + "username": "testuser1", + "password": "password" + }, + "name": { + "title": "Mr", + "first": "John", + "last": "Doe" + }, + "gender": "male", + "location": { + "street": { + "number": 123, + "name": "Main St" + }, + "city": "New York", + "state": "NY", + "country": "USA", + "postcode": "10001", + "coordinates": { + "latitude": 40.7128, + "longitude": -74.006 + }, + "timezone": { + "offset": "-05:00", + "description": "Eastern Time" + } + }, + "email": "john.doe@example.com", + "dob": { + "date": "1990-01-01", + "age": 34 + }, + "registered": { + "date": "2020-01-01", + "age": 4 + }, + "phone": "555-0123", + "cell": "555-0124", + "picture": { + "large": "https://example.com/pic1.jpg", + "medium": "https://example.com/pic1-med.jpg", + "thumbnail": "https://example.com/pic1-thumb.jpg" + }, + "nat": "US" + } + ] +} +``` + +### Get User + +Retrieve a specific user by UUID, username, or email. + +``` +GET /users/:id +``` + +#### Parameters + +| Parameter | Type | Description | +| --------- | ------ | ----------------------------- | +| `id` | string | User UUID, username, or email | + +#### Example Request + +```bash +curl "https://user-api.builder-io.workers.dev/api/users/testuser1" +``` + +### Create User + +Create a new user. + +``` +POST /users +``` + +#### Request Body + +```json +{ + "email": "newuser@example.com", + "login": { + "username": "newuser", + "password": "securepassword" + }, + "name": { + "first": "New", + "last": "User", + "title": "Mr" + }, + "gender": "male", + "location": { + "street": { + "number": 456, + "name": "Oak Avenue" + }, + "city": "Los Angeles", + "state": "CA", + "country": "USA", + "postcode": "90001" + } +} +``` + +#### Required Fields + +- `email` +- `login.username` +- `name.first` +- `name.last` + +#### Example Response + +```json +{ + "success": true, + "uuid": "generated-uuid-here", + "message": "User created successfully" +} +``` + +### Update User + +Update an existing user's information. + +``` +PUT /users/:id +``` + +#### Parameters + +| Parameter | Type | Description | +| --------- | ------ | ----------------------------- | +| `id` | string | User UUID, username, or email | + +#### Request Body + +Include only the fields you want to update: + +```json +{ + "name": { + "first": "Updated" + }, + "location": { + "city": "San Francisco" + } +} +``` + +#### Example Response + +```json +{ + "success": true, + "message": "User updated successfully" +} +``` + +### Delete User + +Delete a user from the system. + +``` +DELETE /users/:id +``` + +#### Parameters + +| Parameter | Type | Description | +| --------- | ------ | ----------------------------- | +| `id` | string | User UUID, username, or email | + +#### Example Response + +```json +{ + "success": true, + "message": "User deleted successfully" +} +``` + +## Error Handling + +All errors return appropriate HTTP status codes with a JSON error response: + +```json +{ + "error": "Error message here" +} +``` + +### Common Status Codes + +- `200` - Success +- `201` - Created +- `400` - Bad Request +- `404` - Not Found +- `405` - Method Not Allowed +- `500` - Internal Server Error + +## CORS + +The API supports CORS with the following headers: + +- `Access-Control-Allow-Origin: *` +- `Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS` +- `Access-Control-Allow-Headers: Content-Type` + +## Rate Limiting + +No rate limiting is currently implemented. + +## Examples + +### Search for users named "John" + +```bash +curl "https://user-api.builder-io.workers.dev/api/users?search=john" +``` + +### Get users sorted by age + +```bash +curl "https://user-api.builder-io.workers.dev/api/users?sortBy=dob.age" +``` + +### Get page 2 with 50 results per page + +```bash +curl "https://user-api.builder-io.workers.dev/api/users?page=2&perPage=50" +``` + +### Create a new user + +```bash +curl -X POST https://user-api.builder-io.workers.dev/api/users \ + -H "Content-Type: application/json" \ + -d '{ + "email": "jane.smith@example.com", + "login": {"username": "janesmith"}, + "name": {"first": "Jane", "last": "Smith"} + }' +``` + +### Update a user's city + +```bash +curl -X PUT https://user-api.builder-io.workers.dev/api/users/janesmith \ + -H "Content-Type: application/json" \ + -d '{"location": {"city": "Boston"}}' +``` + +### Delete a user + +```bash +curl -X DELETE https://user-api.builder-io.workers.dev/api/users/janesmith +``` diff --git a/src/App.tsx b/src/App.tsx index 63aed38..fdea363 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,68 +1,39 @@ import * as React from "react"; -import Container from "@mui/material/Container"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import CssBaseline from "@mui/material/CssBaseline"; import Typography from "@mui/material/Typography"; -import Link from "@mui/material/Link"; -import Slider from "@mui/material/Slider"; -import PopoverMenu from "./PopOverMenu"; -import ProTip from "./ProTip"; -import { BrowserRouter, Routes, Route, Link as RouterLink } from "react-router-dom"; +import Box from "@mui/material/Box"; +import CrmDashboard from "./crm/CrmDashboard"; -function Copyright() { +function NotFound() { return ( - - {"Copyright © "} - - Your Website - {" "} - {new Date().getFullYear()} - {"."} - + + 404: Page Not Found + + + The page you're looking for doesn't exist or has been moved. + + ); } export default function App() { return ( - -
- - Material UI Vite example with Tailwind CSS in TypeScript - - - - - - - - - } - /> - This is the second page!} /> - - -
-
+ + + } /> + } /> +
); } diff --git a/src/crm/CrmDashboard.tsx b/src/crm/CrmDashboard.tsx new file mode 100644 index 0000000..66f922d --- /dev/null +++ b/src/crm/CrmDashboard.tsx @@ -0,0 +1,79 @@ +import * as React from "react"; +import { Outlet, Routes, Route } from "react-router-dom"; +import type {} from "@mui/x-date-pickers/themeAugmentation"; +import type {} from "@mui/x-charts/themeAugmentation"; +import type {} from "@mui/x-data-grid-pro/themeAugmentation"; +import type {} from "@mui/x-tree-view/themeAugmentation"; +import { alpha } from "@mui/material/styles"; +import CssBaseline from "@mui/material/CssBaseline"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import CrmAppNavbar from "./components/CrmAppNavbar"; +import CrmHeader from "./components/CrmHeader"; +import CrmSideMenu from "./components/CrmSideMenu"; +import CrmMainDashboard from "./components/CrmMainDashboard"; +import Customers from "./pages/Customers"; +import Deals from "./pages/Deals"; +import Contacts from "./pages/Contacts"; +import Tasks from "./pages/Tasks"; +import Reports from "./pages/Reports"; +import Settings from "./pages/Settings"; +import AppTheme from "../shared-theme/AppTheme"; +import { + chartsCustomizations, + dataGridCustomizations, + datePickersCustomizations, + treeViewCustomizations, +} from "../dashboard/theme/customizations"; + +const xThemeComponents = { + ...chartsCustomizations, + ...dataGridCustomizations, + ...datePickersCustomizations, + ...treeViewCustomizations, +}; + +export default function CrmDashboard() { + return ( + + + + + + {/* Main content */} + ({ + flexGrow: 1, + backgroundColor: theme.vars + ? `rgba(${theme.vars.palette.background.defaultChannel} / 1)` + : alpha(theme.palette.background.default, 1), + overflow: "auto", + })} + > + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + + ); +} diff --git a/src/crm/components/CrmActivitiesTimeline.tsx b/src/crm/components/CrmActivitiesTimeline.tsx new file mode 100644 index 0000000..eef43eb --- /dev/null +++ b/src/crm/components/CrmActivitiesTimeline.tsx @@ -0,0 +1,121 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import Button from "@mui/material/Button"; +import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded"; +import EmailRoundedIcon from "@mui/icons-material/EmailRounded"; +import PhoneRoundedIcon from "@mui/icons-material/PhoneRounded"; +import MeetingRoomRoundedIcon from "@mui/icons-material/MeetingRoomRounded"; +import EditNoteRoundedIcon from "@mui/icons-material/EditNoteRounded"; + +// Sample activities data +const activities = [ + { + id: 1, + type: "email", + title: "Email sent to Acme Corp", + description: "Proposal follow-up email sent", + time: "11:30 AM", + icon: , + color: "primary", + }, + { + id: 2, + type: "call", + title: "Call with TechSolutions Inc", + description: "Discussed implementation timeline", + time: "10:15 AM", + icon: , + color: "success", + }, + { + id: 3, + type: "meeting", + title: "Meeting scheduled", + description: "Demo for Global Media next Monday", + time: "Yesterday", + icon: , + color: "warning", + }, + { + id: 4, + type: "note", + title: "Note added", + description: "Added details about RetailGiant requirements", + time: "Yesterday", + icon: , + color: "info", + }, +]; + +export default function CrmActivitiesTimeline() { + return ( + + + + + Recent Activities + + + + + + {activities.map((activity) => ( + + + {activity.icon} + + + + + {activity.title} + + + {activity.time} + + + + {activity.description} + + + + ))} + + + + ); +} diff --git a/src/crm/components/CrmAppNavbar.tsx b/src/crm/components/CrmAppNavbar.tsx new file mode 100644 index 0000000..3a05070 --- /dev/null +++ b/src/crm/components/CrmAppNavbar.tsx @@ -0,0 +1,104 @@ +import * as React from "react"; +import { styled } from "@mui/material/styles"; +import AppBar from "@mui/material/AppBar"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import MuiToolbar from "@mui/material/Toolbar"; +import { tabsClasses } from "@mui/material/Tabs"; +import Typography from "@mui/material/Typography"; +import MenuRoundedIcon from "@mui/icons-material/MenuRounded"; +import BusinessRoundedIcon from "@mui/icons-material/BusinessRounded"; +import CrmSideMenuMobile from "./CrmSideMenuMobile"; +import MenuButton from "../../dashboard/components/MenuButton"; +import ColorModeIconDropdown from "../../shared-theme/ColorModeIconDropdown"; + +const Toolbar = styled(MuiToolbar)({ + width: "100%", + padding: "12px", + display: "flex", + flexDirection: "column", + alignItems: "start", + justifyContent: "center", + gap: "12px", + flexShrink: 0, + [`& ${tabsClasses.flexContainer}`]: { + gap: "8px", + p: "8px", + pb: 0, + }, +}); + +export default function CrmAppNavbar() { + const [open, setOpen] = React.useState(false); + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + }; + + return ( + + + + + + + Acme CRM + + + + + + + + + + + ); +} + +export function CrmLogo() { + return ( + + + + ); +} diff --git a/src/crm/components/CrmCustomerDistributionMap.tsx b/src/crm/components/CrmCustomerDistributionMap.tsx new file mode 100644 index 0000000..d380b06 --- /dev/null +++ b/src/crm/components/CrmCustomerDistributionMap.tsx @@ -0,0 +1,87 @@ +import * as React from "react"; +import { useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import Tabs from "@mui/material/Tabs"; +import Tab from "@mui/material/Tab"; + +export default function CrmCustomerDistributionMap() { + const theme = useTheme(); + const [mapView, setMapView] = React.useState("customers"); + + const handleChange = (event: React.SyntheticEvent, newValue: string) => { + setMapView(newValue); + }; + + return ( + + + + + Customer Distribution + + + + + + + + + + + Map visualization would appear here + + + + + ); +} diff --git a/src/crm/components/CrmDateRangePicker.tsx b/src/crm/components/CrmDateRangePicker.tsx new file mode 100644 index 0000000..0462e2d --- /dev/null +++ b/src/crm/components/CrmDateRangePicker.tsx @@ -0,0 +1,88 @@ +import * as React from "react"; +import { styled } from "@mui/material/styles"; +import Button from "@mui/material/Button"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import CalendarTodayRoundedIcon from "@mui/icons-material/CalendarTodayRounded"; +import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded"; + +const StyledButton = styled(Button)(({ theme }) => ({ + textTransform: "none", + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + "&:hover": { + backgroundColor: theme.palette.action.hover, + }, + ...theme.applyStyles("dark", { + backgroundColor: "transparent", + border: `1px solid ${theme.palette.divider}`, + }), +})); + +const dateRanges = [ + { label: "Today", value: "today" }, + { label: "Yesterday", value: "yesterday" }, + { label: "This Week", value: "thisWeek" }, + { label: "Last Week", value: "lastWeek" }, + { label: "This Month", value: "thisMonth" }, + { label: "Last Month", value: "lastMonth" }, + { label: "This Quarter", value: "thisQuarter" }, + { label: "Last Quarter", value: "lastQuarter" }, + { label: "This Year", value: "thisYear" }, + { label: "Custom Range", value: "custom" }, +]; + +export default function CrmDateRangePicker() { + const [anchorEl, setAnchorEl] = React.useState(null); + const [selectedRange, setSelectedRange] = React.useState(dateRanges[4]); // Default to "This Month" + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleRangeSelect = (range: (typeof dateRanges)[0]) => { + setSelectedRange(range); + handleClose(); + }; + + return ( +
+ } + startIcon={} + size="small" + > + {selectedRange.label} + + + {dateRanges.map((range) => ( + handleRangeSelect(range)} + selected={range.value === selectedRange.value} + > + {range.label} + + ))} + +
+ ); +} diff --git a/src/crm/components/CrmHeader.tsx b/src/crm/components/CrmHeader.tsx new file mode 100644 index 0000000..526aad9 --- /dev/null +++ b/src/crm/components/CrmHeader.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import NotificationsRoundedIcon from "@mui/icons-material/NotificationsRounded"; +import MenuButton from "../../dashboard/components/MenuButton"; +import ColorModeIconDropdown from "../../shared-theme/ColorModeIconDropdown"; +import CrmSearch from "./CrmSearch"; +import CrmNavbarBreadcrumbs from "./CrmNavbarBreadcrumbs"; +import Button from "@mui/material/Button"; +import CalendarTodayRoundedIcon from "@mui/icons-material/CalendarTodayRounded"; + +export default function CrmHeader() { + return ( + + + + + CRM Dashboard + + + + + + + + + + + + ); +} diff --git a/src/crm/components/CrmLeadsBySourceChart.tsx b/src/crm/components/CrmLeadsBySourceChart.tsx new file mode 100644 index 0000000..ca751e8 --- /dev/null +++ b/src/crm/components/CrmLeadsBySourceChart.tsx @@ -0,0 +1,76 @@ +import * as React from "react"; +import { useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import { PieChart } from "@mui/x-charts/PieChart"; + +// Sample lead source data +const leadSources = [ + { id: 0, value: 35, label: "Website", color: "#3f51b5" }, + { id: 1, value: 25, label: "Referrals", color: "#2196f3" }, + { id: 2, value: 20, label: "Social Media", color: "#4caf50" }, + { id: 3, value: 15, label: "Email Campaigns", color: "#ff9800" }, + { id: 4, value: 5, label: "Other", color: "#9e9e9e" }, +]; + +export default function CrmLeadsBySourceChart() { + const theme = useTheme(); + + return ( + + + + Leads by Source + + + + `${item.value}%`, + arcLabelMinAngle: 20, + innerRadius: 60, + paddingAngle: 2, + cornerRadius: 4, + valueFormatter: (value) => `${value}%`, + }, + ]} + height={280} + slotProps={{ + legend: { + position: { vertical: "middle", horizontal: "right" }, + direction: "column", + itemMarkWidth: 10, + itemMarkHeight: 10, + markGap: 5, + itemGap: 8, + }, + }} + margin={{ right: 120 }} + /> + + + + ); +} diff --git a/src/crm/components/CrmMainContent.tsx b/src/crm/components/CrmMainContent.tsx new file mode 100644 index 0000000..f85ed8c --- /dev/null +++ b/src/crm/components/CrmMainContent.tsx @@ -0,0 +1,141 @@ +import * as React from "react"; +import Grid from "@mui/material/Grid"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import AddRoundedIcon from "@mui/icons-material/AddRounded"; +import Copyright from "../../dashboard/internals/components/Copyright"; +import CrmStatCard from "./CrmStatCard"; +import CrmRecentDealsTable from "./CrmRecentDealsTable"; +import CrmUpcomingTasks from "./CrmUpcomingTasks"; +import CrmSalesChart from "./CrmSalesChart"; +import CrmLeadsBySourceChart from "./CrmLeadsBySourceChart"; +import CrmActivitiesTimeline from "./CrmActivitiesTimeline"; +import CrmCustomerDistributionMap from "./CrmCustomerDistributionMap"; + +// Sample data for stat cards +const statCardsData = [ + { + title: "Total Customers", + value: "2,543", + interval: "Last 30 days", + trend: "up", + trendValue: "+15%", + data: [ + 200, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440, 460, 480, 500, + 520, 540, 560, 580, 600, 620, 640, 660, 680, 700, 720, 740, 760, 780, 800, + ], + }, + { + title: "Deals Won", + value: "$542K", + interval: "Last 30 days", + trend: "up", + trendValue: "+23%", + data: [ + 400, 420, 440, 460, 480, 500, 520, 540, 560, 580, 600, 620, 640, 660, 680, + 700, 720, 740, 760, 780, 800, 820, 840, 860, 880, 900, 920, 940, 960, 980, + ], + }, + { + title: "New Leads", + value: "456", + interval: "Last 30 days", + trend: "up", + trendValue: "+12%", + data: [ + 300, 310, 320, 330, 340, 350, 360, 370, 380, 390, 400, 410, 420, 430, 440, + 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, + ], + }, + { + title: "Conversion Rate", + value: "28%", + interval: "Last 30 days", + trend: "down", + trendValue: "-5%", + data: [ + 35, 33, 32, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 29, 28, 27, 26, 25, 24, 23, 22, + ], + }, +]; + +export default function CrmMainContent() { + return ( + + {/* Header with action buttons */} + + + Dashboard Overview + + + + + + + + {/* Stats Cards row */} + + {statCardsData.map((card, index) => ( + + + + ))} + + + {/* Charts row */} + + + + + + + + + + {/* Tables & Other content */} + + + + + + + + + + + + + {/* Map row */} + + + + + + + + + ); +} diff --git a/src/crm/components/CrmMainDashboard.tsx b/src/crm/components/CrmMainDashboard.tsx new file mode 100644 index 0000000..171117e --- /dev/null +++ b/src/crm/components/CrmMainDashboard.tsx @@ -0,0 +1,131 @@ +import * as React from "react"; +import Grid from "@mui/material/Grid"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import AddRoundedIcon from "@mui/icons-material/AddRounded"; +import Copyright from "../../dashboard/internals/components/Copyright"; +import CrmStatCard from "./CrmStatCard"; +import CrmRecentDealsTable from "./CrmRecentDealsTable"; +import CrmUpcomingTasks from "./CrmUpcomingTasks"; +import CrmSalesChart from "./CrmSalesChart"; +import CrmLeadsBySourceChart from "./CrmLeadsBySourceChart"; + +// Sample data for stat cards +const statCardsData = [ + { + title: "Total Customers", + value: "2,543", + interval: "Last 30 days", + trend: "up", + trendValue: "+15%", + data: [ + 200, 240, 260, 280, 300, 320, 340, 360, 380, 400, 420, 440, 460, 480, 500, + 520, 540, 560, 580, 600, 620, 640, 660, 680, 700, 720, 740, 760, 780, 800, + ], + }, + { + title: "Deals Won", + value: "$542K", + interval: "Last 30 days", + trend: "up", + trendValue: "+23%", + data: [ + 400, 420, 440, 460, 480, 500, 520, 540, 560, 580, 600, 620, 640, 660, 680, + 700, 720, 740, 760, 780, 800, 820, 840, 860, 880, 900, 920, 940, 960, 980, + ], + }, + { + title: "New Leads", + value: "456", + interval: "Last 30 days", + trend: "up", + trendValue: "+12%", + data: [ + 300, 310, 320, 330, 340, 350, 360, 370, 380, 390, 400, 410, 420, 430, 440, + 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, + ], + }, + { + title: "Conversion Rate", + value: "28%", + interval: "Last 30 days", + trend: "down", + trendValue: "-5%", + data: [ + 35, 33, 32, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 29, 28, 27, 26, 25, 24, 23, 22, + ], + }, +]; + +export default function CrmMainDashboard() { + return ( + + {/* Header with action buttons */} + + + Dashboard Overview + + + + + + + + {/* Stats Cards row */} + + {statCardsData.map((card, index) => ( + + + + ))} + + + {/* Charts row */} + + + + + + + + + + {/* Tables & Other content */} + + + + + + + + + + + + + + ); +} diff --git a/src/crm/components/CrmMenuContent.tsx b/src/crm/components/CrmMenuContent.tsx new file mode 100644 index 0000000..658ea80 --- /dev/null +++ b/src/crm/components/CrmMenuContent.tsx @@ -0,0 +1,75 @@ +import * as React from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import Box from "@mui/material/Box"; // Added the missing import +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Stack from "@mui/material/Stack"; +import Divider from "@mui/material/Divider"; +import DashboardRoundedIcon from "@mui/icons-material/DashboardRounded"; +import PeopleRoundedIcon from "@mui/icons-material/PeopleRounded"; +import BusinessCenterRoundedIcon from "@mui/icons-material/BusinessCenterRounded"; +import ContactsRoundedIcon from "@mui/icons-material/ContactsRounded"; +import AssignmentRoundedIcon from "@mui/icons-material/AssignmentRounded"; +import AssessmentRoundedIcon from "@mui/icons-material/AssessmentRounded"; +import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded"; +import HelpOutlineRoundedIcon from "@mui/icons-material/HelpOutlineRounded"; + +const mainListItems = [ + { text: "Dashboard", icon: , path: "/" }, + { text: "Customers", icon: , path: "/customers" }, + { text: "Deals", icon: , path: "/deals" }, + { text: "Contacts", icon: , path: "/contacts" }, + { text: "Tasks", icon: , path: "/tasks" }, + { text: "Reports", icon: , path: "/reports" }, +]; + +const secondaryListItems = [ + { text: "Settings", icon: , path: "/settings" }, + { text: "Help & Support", icon: , path: "/help" }, +]; + +export default function CrmMenuContent() { + const navigate = useNavigate(); + const location = useLocation(); + + const handleNavigation = (path: string) => { + navigate(path); + }; + + return ( + + + {mainListItems.map((item, index) => ( + + handleNavigation(item.path)} + > + {item.icon} + + + + ))} + + + + + {secondaryListItems.map((item, index) => ( + + handleNavigation(item.path)} + > + {item.icon} + + + + ))} + + + + ); +} diff --git a/src/crm/components/CrmNavbarBreadcrumbs.tsx b/src/crm/components/CrmNavbarBreadcrumbs.tsx new file mode 100644 index 0000000..4e24d51 --- /dev/null +++ b/src/crm/components/CrmNavbarBreadcrumbs.tsx @@ -0,0 +1,55 @@ +import * as React from "react"; +import { useLocation, Link as RouterLink } from "react-router-dom"; +import Breadcrumbs from "@mui/material/Breadcrumbs"; +import Link from "@mui/material/Link"; +import Typography from "@mui/material/Typography"; +import HomeRoundedIcon from "@mui/icons-material/HomeRounded"; +import NavigateNextRoundedIcon from "@mui/icons-material/NavigateNextRounded"; + +function capitalizeFirstLetter(string: string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +export default function CrmNavbarBreadcrumbs() { + const location = useLocation(); + const pathnames = location.pathname.split("/").filter((x) => x); + + return ( + } + aria-label="breadcrumb" + sx={{ mb: 1 }} + > + + + Home + + {pathnames.map((value, index) => { + const last = index === pathnames.length - 1; + const to = `/${pathnames.slice(0, index + 1).join("/")}`; + + return last ? ( + + {capitalizeFirstLetter(value)} + + ) : ( + + {capitalizeFirstLetter(value)} + + ); + })} + + ); +} diff --git a/src/crm/components/CrmOptionsMenu.tsx b/src/crm/components/CrmOptionsMenu.tsx new file mode 100644 index 0000000..941253f --- /dev/null +++ b/src/crm/components/CrmOptionsMenu.tsx @@ -0,0 +1,68 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Divider from "@mui/material/Divider"; +import MoreVertRoundedIcon from "@mui/icons-material/MoreVertRounded"; +import PersonRoundedIcon from "@mui/icons-material/PersonRounded"; +import ExitToAppRoundedIcon from "@mui/icons-material/ExitToAppRounded"; +import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded"; + +export default function CrmOptionsMenu() { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + + + + + + + + + + My Profile + + + + + + Account Settings + + + + + + + Sign Out + + + + ); +} diff --git a/src/crm/components/CrmRecentDealsTable.tsx b/src/crm/components/CrmRecentDealsTable.tsx new file mode 100644 index 0000000..a191866 --- /dev/null +++ b/src/crm/components/CrmRecentDealsTable.tsx @@ -0,0 +1,186 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import Typography from "@mui/material/Typography"; +import Chip from "@mui/material/Chip"; +import Stack from "@mui/material/Stack"; +import Avatar from "@mui/material/Avatar"; +import IconButton from "@mui/material/IconButton"; +import MoreVertRoundedIcon from "@mui/icons-material/MoreVertRounded"; +import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded"; +import Button from "@mui/material/Button"; + +// Sample data for recent deals +const recentDeals = [ + { + id: 1, + name: "Enterprise Software Package", + customer: { name: "Acme Corp", avatar: "A" }, + value: 125000, + stage: "Proposal", + probability: 75, + closingDate: "2023-09-30", + }, + { + id: 2, + name: "Cloud Migration Service", + customer: { name: "TechSolutions Inc", avatar: "T" }, + value: 87500, + stage: "Negotiation", + probability: 90, + closingDate: "2023-10-15", + }, + { + id: 3, + name: "Website Redesign Project", + customer: { name: "Global Media", avatar: "G" }, + value: 45000, + stage: "Discovery", + probability: 60, + closingDate: "2023-11-05", + }, + { + id: 4, + name: "CRM Implementation", + customer: { name: "RetailGiant", avatar: "R" }, + value: 95000, + stage: "Closed Won", + probability: 100, + closingDate: "2023-09-15", + }, + { + id: 5, + name: "IT Infrastructure Upgrade", + customer: { name: "HealthCare Pro", avatar: "H" }, + value: 135000, + stage: "Negotiation", + probability: 85, + closingDate: "2023-10-22", + }, +]; + +// Function to get color based on deal stage +const getStageColor = ( + stage: string, +): "default" | "primary" | "success" | "warning" | "info" => { + switch (stage) { + case "Discovery": + return "info"; + case "Proposal": + return "primary"; + case "Negotiation": + return "warning"; + case "Closed Won": + return "success"; + default: + return "default"; + } +}; + +// Format currency +const formatCurrency = (value: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format(value); +}; + +// Format date +const formatDate = (dateString: string) => { + const options: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "short", + day: "numeric", + }; + return new Date(dateString).toLocaleDateString("en-US", options); +}; + +export default function CrmRecentDealsTable() { + return ( + + + + + Recent Deals + + + + + + + + + Deal Name + Customer + Value + Stage + Probability + Closing Date + Actions + + + + {recentDeals.map((deal) => ( + + {deal.name} + + + + {deal.customer.avatar} + + + {deal.customer.name} + + + + + {formatCurrency(deal.value)} + + + + + {deal.probability}% + {formatDate(deal.closingDate)} + + + + + + + ))} + +
+
+
+ ); +} diff --git a/src/crm/components/CrmSalesChart.tsx b/src/crm/components/CrmSalesChart.tsx new file mode 100644 index 0000000..38ebdcb --- /dev/null +++ b/src/crm/components/CrmSalesChart.tsx @@ -0,0 +1,173 @@ +import * as React from "react"; +import { useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; +import ToggleButton from "@mui/material/ToggleButton"; +import { BarChart } from "@mui/x-charts/BarChart"; + +export default function CrmSalesChart() { + const theme = useTheme(); + const [timeRange, setTimeRange] = React.useState("year"); + + const handleTimeRangeChange = ( + event: React.MouseEvent, + newTimeRange: string | null, + ) => { + if (newTimeRange !== null) { + setTimeRange(newTimeRange); + } + }; + + // Generate monthly data + const currentYear = new Date().getFullYear(); + const monthNames = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + + // Sample data (in a real app this would come from an API) + const salesData = [ + 180000, 210000, 250000, 220000, 270000, 310000, 330000, 350000, 390000, + 410000, 430000, 470000, + ]; + const targetsData = [ + 200000, 220000, 240000, 260000, 280000, 300000, 320000, 340000, 360000, + 380000, 400000, 450000, + ]; + const projectedData = [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 450000, + 500000, + ]; + + const xAxisData = { + scaleType: "band" as const, + data: monthNames, + tickLabelStyle: { + angle: 0, + textAnchor: "middle", + fontSize: 12, + }, + }; + + // Format y-axis labels to show $ and K for thousands + const formatYAxis = (value: number) => { + if (value >= 1000000) { + return `$${(value / 1000000).toFixed(1)}M`; + } + if (value >= 1000) { + return `$${(value / 1000).toFixed(0)}K`; + } + return `$${value}`; + }; + + return ( + + + + + Sales Performance + + + + Month + + + Quarter + + + Year + + + + + + (value ? formatYAxis(value) : ""), + }, + { + data: targetsData, + label: "Targets", + color: theme.palette.grey[400], + valueFormatter: (value) => (value ? formatYAxis(value) : ""), + }, + { + data: projectedData, + label: "Projected", + color: theme.palette.secondary.main, + valueFormatter: (value) => (value ? formatYAxis(value) : ""), + }, + ]} + xAxis={[xAxisData]} + yAxis={[ + { + label: "Revenue", + valueFormatter: formatYAxis, + }, + ]} + height={300} + margin={{ top: 10, bottom: 30, left: 60, right: 10 }} + slotProps={{ + legend: { + position: { vertical: "top", horizontal: "middle" }, + itemMarkWidth: 10, + itemMarkHeight: 10, + markGap: 5, + itemGap: 10, + }, + }} + /> + + + + ); +} diff --git a/src/crm/components/CrmSearch.tsx b/src/crm/components/CrmSearch.tsx new file mode 100644 index 0000000..dca0de4 --- /dev/null +++ b/src/crm/components/CrmSearch.tsx @@ -0,0 +1,65 @@ +import * as React from "react"; +import InputBase from "@mui/material/InputBase"; +import SearchRoundedIcon from "@mui/icons-material/SearchRounded"; +import { alpha, styled } from "@mui/material/styles"; + +const SearchWrapper = styled("div")(({ theme }) => ({ + position: "relative", + borderRadius: theme.shape.borderRadius, + backgroundColor: alpha(theme.palette.common.black, 0.04), + "&:hover": { + backgroundColor: alpha(theme.palette.common.black, 0.06), + }, + marginLeft: 0, + width: "100%", + [theme.breakpoints.up("sm")]: { + width: "auto", + marginLeft: theme.spacing(1), + }, + ...theme.applyStyles("dark", { + backgroundColor: alpha(theme.palette.common.white, 0.06), + "&:hover": { + backgroundColor: alpha(theme.palette.common.white, 0.1), + }, + }), +})); + +const SearchIconWrapper = styled("div")(({ theme }) => ({ + padding: theme.spacing(0, 2), + height: "100%", + position: "absolute", + pointerEvents: "none", + display: "flex", + alignItems: "center", + justifyContent: "center", +})); + +const StyledInputBase = styled(InputBase)(({ theme }) => ({ + color: "inherit", + "& .MuiInputBase-input": { + padding: theme.spacing(1, 1, 1, 0), + paddingLeft: `calc(1em + ${theme.spacing(4)})`, + transition: theme.transitions.create("width"), + width: "100%", + [theme.breakpoints.up("sm")]: { + width: "12ch", + "&:focus": { + width: "20ch", + }, + }, + }, +})); + +export default function CrmSearch() { + return ( + + + + + + + ); +} diff --git a/src/crm/components/CrmSelectCompany.tsx b/src/crm/components/CrmSelectCompany.tsx new file mode 100644 index 0000000..b9aaaed --- /dev/null +++ b/src/crm/components/CrmSelectCompany.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import MenuItem from "@mui/material/MenuItem"; +import FormControl from "@mui/material/FormControl"; +import Select, { SelectChangeEvent } from "@mui/material/Select"; +import BusinessRoundedIcon from "@mui/icons-material/BusinessRounded"; + +export default function CrmSelectCompany() { + const [company, setCompany] = React.useState("acme"); + + const handleChange = (event: SelectChangeEvent) => { + setCompany(event.target.value as string); + }; + + return ( + + + + + + ); +} diff --git a/src/crm/components/CrmSideMenu.tsx b/src/crm/components/CrmSideMenu.tsx new file mode 100644 index 0000000..e9dfd08 --- /dev/null +++ b/src/crm/components/CrmSideMenu.tsx @@ -0,0 +1,91 @@ +import * as React from "react"; +import { styled } from "@mui/material/styles"; +import { useNavigate, useLocation } from "react-router-dom"; +import Avatar from "@mui/material/Avatar"; +import MuiDrawer, { drawerClasses } from "@mui/material/Drawer"; +import Box from "@mui/material/Box"; +import Divider from "@mui/material/Divider"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import CrmSelectCompany from "./CrmSelectCompany"; +import CrmMenuContent from "./CrmMenuContent"; +import CrmOptionsMenu from "./CrmOptionsMenu"; + +const drawerWidth = 240; + +const Drawer = styled(MuiDrawer)({ + width: drawerWidth, + flexShrink: 0, + boxSizing: "border-box", + mt: 10, + [`& .${drawerClasses.paper}`]: { + width: drawerWidth, + boxSizing: "border-box", + }, +}); + +export default function CrmSideMenu() { + return ( + + + + + + + + + + + AT + + + + Alex Thompson + + + alex@acmecrm.com + + + + + + ); +} diff --git a/src/crm/components/CrmSideMenuMobile.tsx b/src/crm/components/CrmSideMenuMobile.tsx new file mode 100644 index 0000000..8f5eaca --- /dev/null +++ b/src/crm/components/CrmSideMenuMobile.tsx @@ -0,0 +1,124 @@ +import * as React from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import Box from "@mui/material/Box"; +import Drawer from "@mui/material/Drawer"; +import Divider from "@mui/material/Divider"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Typography from "@mui/material/Typography"; +import DashboardRoundedIcon from "@mui/icons-material/DashboardRounded"; +import PeopleRoundedIcon from "@mui/icons-material/PeopleRounded"; +import BusinessCenterRoundedIcon from "@mui/icons-material/BusinessCenterRounded"; +import ContactsRoundedIcon from "@mui/icons-material/ContactsRounded"; +import AssignmentRoundedIcon from "@mui/icons-material/AssignmentRounded"; +import AssessmentRoundedIcon from "@mui/icons-material/AssessmentRounded"; +import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded"; +import HelpOutlineRoundedIcon from "@mui/icons-material/HelpOutlineRounded"; +import { CrmLogo } from "./CrmAppNavbar"; + +const mainListItems = [ + { text: "Dashboard", icon: , path: "/" }, + { text: "Customers", icon: , path: "/customers" }, + { text: "Deals", icon: , path: "/deals" }, + { text: "Contacts", icon: , path: "/contacts" }, + { text: "Tasks", icon: , path: "/tasks" }, + { text: "Reports", icon: , path: "/reports" }, +]; + +const secondaryListItems = [ + { text: "Settings", icon: , path: "/settings" }, + { text: "Help & Support", icon: , path: "/help" }, +]; + +interface CrmSideMenuMobileProps { + open: boolean; + toggleDrawer: (open: boolean) => () => void; +} + +export default function CrmSideMenuMobile({ + open, + toggleDrawer, +}: CrmSideMenuMobileProps) { + const navigate = useNavigate(); + const location = useLocation(); + + const handleNavigation = (path: string) => { + navigate(path); + toggleDrawer(false)(); + }; + + return ( + + + + + + Acme CRM + + + + + {mainListItems.map((item, index) => ( + + handleNavigation(item.path)} + > + {item.icon} + + + + ))} + + + + + + {secondaryListItems.map((item, index) => ( + + handleNavigation(item.path)} + > + {item.icon} + + + + ))} + + + + ); +} diff --git a/src/crm/components/CrmStatCard.tsx b/src/crm/components/CrmStatCard.tsx new file mode 100644 index 0000000..98ee8f0 --- /dev/null +++ b/src/crm/components/CrmStatCard.tsx @@ -0,0 +1,139 @@ +import * as React from "react"; +import { useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Chip from "@mui/material/Chip"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; +import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; +import { SparkLineChart } from "@mui/x-charts/SparkLineChart"; +import { areaElementClasses } from "@mui/x-charts/LineChart"; + +export type CrmStatCardProps = { + title: string; + value: string; + interval: string; + trend: "up" | "down"; + trendValue: string; + data: number[]; +}; + +function AreaGradient({ color, id }: { color: string; id: string }) { + return ( + + + + + + + ); +} + +export default function CrmStatCard({ + title, + value, + interval, + trend, + trendValue, + data, +}: CrmStatCardProps) { + const theme = useTheme(); + + const trendColors = { + up: + theme.palette.mode === "light" + ? theme.palette.success.main + : theme.palette.success.dark, + down: + theme.palette.mode === "light" + ? theme.palette.error.main + : theme.palette.error.dark, + }; + + const labelColors = { + up: "success" as const, + down: "error" as const, + }; + + const trendIcons = { + up: , + down: , + }; + + const color = labelColors[trend]; + const chartColor = trendColors[trend]; + const trendIcon = trendIcons[trend]; + + return ( + + + + {title} + + + + + + {value} + + + + + {interval} + + + + `Day ${i + 1}`, + ), + }} + sx={{ + [`& .${areaElementClasses.root}`]: { + fill: `url(#area-gradient-${title.replace(/\s+/g, "-").toLowerCase()})`, + }, + }} + > + + + + + + + ); +} diff --git a/src/crm/components/CrmUpcomingTasks.tsx b/src/crm/components/CrmUpcomingTasks.tsx new file mode 100644 index 0000000..18e605a --- /dev/null +++ b/src/crm/components/CrmUpcomingTasks.tsx @@ -0,0 +1,184 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import Checkbox from "@mui/material/Checkbox"; +import IconButton from "@mui/material/IconButton"; +import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Chip from "@mui/material/Chip"; + +// Sample data for upcoming tasks +const upcomingTasks = [ + { + id: 1, + task: "Follow up with TechSolutions Inc on cloud proposal", + completed: false, + priority: "high", + dueDate: "Today, 2:00 PM", + }, + { + id: 2, + task: "Prepare presentation for Global Media website project", + completed: false, + priority: "medium", + dueDate: "Tomorrow, 10:00 AM", + }, + { + id: 3, + task: "Call HealthCare Pro about contract details", + completed: false, + priority: "high", + dueDate: "Today, 4:30 PM", + }, + { + id: 4, + task: "Update CRM implementation timeline for RetailGiant", + completed: true, + priority: "medium", + dueDate: "Yesterday", + }, + { + id: 5, + task: "Send proposal documents to Acme Corp", + completed: false, + priority: "low", + dueDate: "Sep 28, 2023", + }, +]; + +// Function to get priority color +const getPriorityColor = ( + priority: string, +): "error" | "warning" | "default" => { + switch (priority) { + case "high": + return "error"; + case "medium": + return "warning"; + default: + return "default"; + } +}; + +export default function CrmUpcomingTasks() { + const [tasks, setTasks] = React.useState(upcomingTasks); + + const handleToggle = (id: number) => () => { + setTasks( + tasks.map((task) => + task.id === id ? { ...task, completed: !task.completed } : task, + ), + ); + }; + + return ( + + + + + Upcoming Tasks + + + + + + {tasks.map((task) => { + const labelId = `checkbox-list-label-${task.id}`; + + return ( + + + + } + disablePadding + > + + + + + + {task.task} + + } + secondary={ + + + + {task.dueDate} + + + } + /> + + + ); + })} + + + + ); +} diff --git a/src/crm/components/CustomerDataGrid.tsx b/src/crm/components/CustomerDataGrid.tsx new file mode 100644 index 0000000..bbde811 --- /dev/null +++ b/src/crm/components/CustomerDataGrid.tsx @@ -0,0 +1,392 @@ +import * as React from "react"; +import { useState, useCallback, useMemo } from "react"; +import { + DataGrid, + GridColDef, + GridRowParams, + GridPaginationModel, + GridSortModel, +} from "@mui/x-data-grid"; +import { + Avatar, + Chip, + Box, + Typography, + IconButton, + Menu, + MenuItem, + Stack, + TextField, + InputAdornment, + Card, + Alert, + CircularProgress, +} from "@mui/material"; +import { + Edit as EditIcon, + Delete as DeleteIcon, + MoreVert as MoreVertIcon, + Search as SearchIcon, + Clear as ClearIcon, +} from "@mui/icons-material"; +import { User } from "../types/user"; + +interface CustomerDataGridProps { + users: User[]; + loading: boolean; + error: string | null; + pagination: { + page: number; + perPage: number; + total: number; + }; + onPaginationChange: (page: number, perPage: number) => void; + onSearch: (search: string) => void; + onSort: (sortBy: string) => void; + onEdit: (user: User) => void; + onDelete: (userId: string) => void; +} + +export default function CustomerDataGrid({ + users, + loading, + error, + pagination, + onPaginationChange, + onSearch, + onSort, + onEdit, + onDelete, +}: CustomerDataGridProps) { + const [searchValue, setSearchValue] = useState(""); + const [sortModel, setSortModel] = useState([]); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedUser, setSelectedUser] = useState(null); + + const handleSearchChange = useCallback( + (event: React.ChangeEvent) => { + const value = event.target.value; + setSearchValue(value); + onSearch(value); + }, + [onSearch], + ); + + const handleClearSearch = useCallback(() => { + setSearchValue(""); + onSearch(""); + }, [onSearch]); + + const handlePaginationModelChange = useCallback( + (model: GridPaginationModel) => { + onPaginationChange(model.page + 1, model.pageSize); + }, + [onPaginationChange], + ); + + const handleSortModelChange = useCallback( + (model: GridSortModel) => { + setSortModel(model); + if (model.length > 0) { + const sort = model[0]; + const sortBy = + sort.field === "fullName" + ? "name.first" + : sort.field === "email" + ? "email" + : sort.field === "city" + ? "location.city" + : sort.field === "country" + ? "location.country" + : sort.field === "age" + ? "dob.age" + : "name.first"; + onSort(sortBy); + } + }, + [onSort], + ); + + const handleMenuClick = ( + event: React.MouseEvent, + user: User, + ) => { + setAnchorEl(event.currentTarget); + setSelectedUser(user); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedUser(null); + }; + + const handleEdit = () => { + if (selectedUser) { + onEdit(selectedUser); + } + handleMenuClose(); + }; + + const handleDelete = () => { + if (selectedUser) { + onDelete(selectedUser.login.uuid); + } + handleMenuClose(); + }; + + const columns: GridColDef[] = useMemo( + () => [ + { + field: "fullName", + headerName: "Customer", + flex: 1.3, + minWidth: 180, + renderCell: (params) => { + const user = params.row as User; + const initials = + `${user.name.first[0]}${user.name.last[0]}`.toUpperCase(); + + return ( + + + {initials} + + + {user.name.title} {user.name.first} {user.name.last} + + + ); + }, + sortable: true, + }, + { + field: "email", + headerName: "Email", + flex: 1.2, + minWidth: 180, + sortable: true, + }, + { + field: "gender", + headerName: "Gender", + width: 90, + renderCell: (params) => { + const gender = params.value as string; + const color = + gender === "male" + ? "primary" + : gender === "female" + ? "secondary" + : "default"; + return ( + + ); + }, + sortable: false, + }, + { + field: "age", + headerName: "Age", + width: 70, + align: "center", + headerAlign: "center", + valueGetter: (value, row) => row.dob.age, + sortable: true, + }, + { + field: "location", + headerName: "Location", + flex: 1, + minWidth: 140, + renderCell: (params) => { + const user = params.row as User; + return ( + + {user.location.city}, {user.location.country} + + ); + }, + sortable: false, + }, + { + field: "phone", + headerName: "Phone", + width: 130, + sortable: false, + }, + { + field: "registered", + headerName: "Member Since", + width: 110, + valueGetter: (value, row) => { + const date = new Date(row.registered.date); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + }); + }, + sortable: true, + }, + { + field: "actions", + type: "actions", + headerName: "Actions", + width: 70, + renderCell: (params) => { + const user = params.row as User; + return ( + handleMenuClick(event, user)} + aria-label="user actions" + > + + + ); + }, + }, + ], + [], + ); + + const rows = useMemo( + () => + users.map((user) => ({ + id: user.login.uuid, + ...user, + })), + [users], + ); + + if (error) { + return ( + + {error} + + ); + } + + return ( + + {/* Search Header */} + + + + + + ), + endAdornment: searchValue && ( + + + + + + ), + }} + /> + + {pagination.total} customers total + + + + + {/* Data Grid */} + + {loading && ( + + + + )} + + + params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd" + } + slotProps={{ + pagination: { + showFirstButton: true, + showLastButton: true, + }, + }} + sx={{ + border: "none", + "& .MuiDataGrid-cell": { + borderBottom: "1px solid", + borderColor: "divider", + }, + "& .MuiDataGrid-row:hover": { + backgroundColor: "action.hover", + }, + }} + /> + + + {/* Actions Menu */} + + + + Edit Customer + + + + Delete Customer + + + + ); +} diff --git a/src/crm/components/CustomerEditModal.tsx b/src/crm/components/CustomerEditModal.tsx new file mode 100644 index 0000000..a1fab12 --- /dev/null +++ b/src/crm/components/CustomerEditModal.tsx @@ -0,0 +1,380 @@ +import * as React from "react"; +import { useState, useEffect } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Grid, + FormControl, + InputLabel, + Select, + MenuItem, + CircularProgress, + Alert, + Stack, + Typography, + Divider, +} from "@mui/material"; +import { User, UserFormData } from "../types/user"; + +interface CustomerEditModalProps { + open: boolean; + onClose: () => void; + user: User | null; + onSave: ( + userId: string, + userData: Partial, + ) => Promise<{ success: boolean; error?: string }>; + loading: boolean; +} + +export default function CustomerEditModal({ + open, + onClose, + user, + onSave, + loading, +}: CustomerEditModalProps) { + const [formData, setFormData] = useState({ + email: "", + name: { + title: "", + first: "", + last: "", + }, + gender: "", + location: { + street: { + number: 0, + name: "", + }, + city: "", + state: "", + country: "", + postcode: "", + }, + phone: "", + cell: "", + }); + + const [saveError, setSaveError] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (user) { + setFormData({ + email: user.email, + name: { + title: user.name.title, + first: user.name.first, + last: user.name.last, + }, + gender: user.gender, + location: { + street: { + number: user.location.street.number, + name: user.location.street.name, + }, + city: user.location.city, + state: user.location.state, + country: user.location.country, + postcode: user.location.postcode, + }, + phone: user.phone, + cell: user.cell, + }); + } + setSaveError(null); + }, [user]); + + const handleInputChange = (field: string, value: any) => { + setFormData((prev) => { + const keys = field.split("."); + if (keys.length === 1) { + return { ...prev, [field]: value }; + } + + // Handle nested properties + const newData = { ...prev }; + let current: any = newData; + + for (let i = 0; i < keys.length - 1; i++) { + current = current[keys[i]]; + } + current[keys[keys.length - 1]] = value; + + return newData; + }); + }; + + const handleSave = async () => { + if (!user) return; + + setSaving(true); + setSaveError(null); + + try { + const result = await onSave(user.login.uuid, formData); + if (result.success) { + onClose(); + } else { + setSaveError(result.error || "Failed to update user"); + } + } catch (error) { + setSaveError("An unexpected error occurred"); + } finally { + setSaving(false); + } + }; + + const handleClose = () => { + setSaveError(null); + onClose(); + }; + + if (!user) return null; + + return ( + + + + Edit Customer: {user.name.first} {user.name.last} + + + + + + {saveError && ( + setSaveError(null)}> + {saveError} + + )} + + {/* Personal Information */} +
+ + Personal Information + + + + + Title + + + + + + handleInputChange("name.first", e.target.value) + } + required + /> + + + + handleInputChange("name.last", e.target.value) + } + required + /> + + + handleInputChange("email", e.target.value)} + required + /> + + + + Gender + + + + +
+ + + + {/* Contact Information */} +
+ + Contact Information + + + + handleInputChange("phone", e.target.value)} + /> + + + handleInputChange("cell", e.target.value)} + /> + + +
+ + + + {/* Address Information */} +
+ + Address Information + + + + + handleInputChange( + "location.street.number", + parseInt(e.target.value) || 0, + ) + } + /> + + + + handleInputChange("location.street.name", e.target.value) + } + /> + + + + handleInputChange("location.city", e.target.value) + } + /> + + + + handleInputChange("location.state", e.target.value) + } + /> + + + + handleInputChange("location.postcode", e.target.value) + } + /> + + + + handleInputChange("location.country", e.target.value) + } + /> + + +
+
+
+ + + + + +
+ ); +} diff --git a/src/crm/hooks/useCustomers.ts b/src/crm/hooks/useCustomers.ts new file mode 100644 index 0000000..319e438 --- /dev/null +++ b/src/crm/hooks/useCustomers.ts @@ -0,0 +1,151 @@ +import { useState, useEffect, useCallback } from "react"; +import { User, UsersApiResponse, UserFormData } from "../types/user"; + +const API_BASE_URL = "https://user-api.builder-io.workers.dev/api"; + +export const useCustomers = () => { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [pagination, setPagination] = useState({ + page: 1, + perPage: 20, + total: 0, + }); + + const fetchUsers = useCallback( + async ( + page: number = 1, + perPage: number = 20, + search: string = "", + sortBy: string = "name.first", + ) => { + setLoading(true); + setError(null); + + try { + const params = new URLSearchParams({ + page: page.toString(), + perPage: perPage.toString(), + ...(search && { search }), + sortBy, + }); + + const response = await fetch(`${API_BASE_URL}/users?${params}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: UsersApiResponse = await response.json(); + + setUsers(data.data); + setPagination({ + page: data.page, + perPage: data.perPage, + total: data.total, + }); + } catch (err) { + const errorMessage = + err instanceof Error + ? err.message + : "An error occurred while fetching users"; + setError(errorMessage); + console.error("Error fetching users:", err); + } finally { + setLoading(false); + } + }, + [], + ); + + const updateUser = useCallback( + async (userId: string, userData: Partial) => { + setLoading(true); + setError(null); + + try { + const response = await fetch(`${API_BASE_URL}/users/${userId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + errorData.error || `HTTP error! status: ${response.status}`, + ); + } + + // Refresh the users list after successful update + await fetchUsers(pagination.page, pagination.perPage); + + return { success: true }; + } catch (err) { + const errorMessage = + err instanceof Error + ? err.message + : "An error occurred while updating user"; + setError(errorMessage); + console.error("Error updating user:", err); + return { success: false, error: errorMessage }; + } finally { + setLoading(false); + } + }, + [pagination.page, pagination.perPage, fetchUsers], + ); + + const deleteUser = useCallback( + async (userId: string) => { + setLoading(true); + setError(null); + + try { + const response = await fetch(`${API_BASE_URL}/users/${userId}`, { + method: "DELETE", + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + errorData.error || `HTTP error! status: ${response.status}`, + ); + } + + // Refresh the users list after successful deletion + await fetchUsers(pagination.page, pagination.perPage); + + return { success: true }; + } catch (err) { + const errorMessage = + err instanceof Error + ? err.message + : "An error occurred while deleting user"; + setError(errorMessage); + console.error("Error deleting user:", err); + return { success: false, error: errorMessage }; + } finally { + setLoading(false); + } + }, + [pagination.page, pagination.perPage, fetchUsers], + ); + + useEffect(() => { + fetchUsers(); + }, [fetchUsers]); + + return { + users, + loading, + error, + pagination, + fetchUsers, + updateUser, + deleteUser, + }; +}; diff --git a/src/crm/pages/Contacts.tsx b/src/crm/pages/Contacts.tsx new file mode 100644 index 0000000..f9038c6 --- /dev/null +++ b/src/crm/pages/Contacts.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +export default function Contacts() { + return ( + + + Contacts Page + + + This is the contacts management page where you can organize and manage + your business contacts. + + + ); +} diff --git a/src/crm/pages/Customers.tsx b/src/crm/pages/Customers.tsx new file mode 100644 index 0000000..a433bfc --- /dev/null +++ b/src/crm/pages/Customers.tsx @@ -0,0 +1,237 @@ +import * as React from "react"; +import { useState, useCallback } from "react"; +import { + Box, + Typography, + Stack, + Alert, + Snackbar, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, +} from "@mui/material"; +import CustomerDataGrid from "../components/CustomerDataGrid"; +import CustomerEditModal from "../components/CustomerEditModal"; +import { useCustomers } from "../hooks/useCustomers"; +import { User } from "../types/user"; + +export default function Customers() { + const { + users, + loading, + error, + pagination, + fetchUsers, + updateUser, + deleteUser, + } = useCustomers(); + + const [selectedUser, setSelectedUser] = useState(null); + const [editModalOpen, setEditModalOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [userToDelete, setUserToDelete] = useState(null); + const [snackbar, setSnackbar] = useState<{ + open: boolean; + message: string; + severity: "success" | "error"; + }>({ + open: false, + message: "", + severity: "success", + }); + + const [searchTerm, setSearchTerm] = useState(""); + const [sortBy, setSortBy] = useState("name.first"); + + const handlePaginationChange = useCallback( + (page: number, perPage: number) => { + fetchUsers(page, perPage, searchTerm, sortBy); + }, + [fetchUsers, searchTerm, sortBy], + ); + + const handleSearch = useCallback( + (search: string) => { + setSearchTerm(search); + fetchUsers(1, pagination.perPage, search, sortBy); + }, + [fetchUsers, pagination.perPage, sortBy], + ); + + const handleSort = useCallback( + (newSortBy: string) => { + setSortBy(newSortBy); + fetchUsers(pagination.page, pagination.perPage, searchTerm, newSortBy); + }, + [fetchUsers, pagination.page, pagination.perPage, searchTerm], + ); + + const handleEdit = useCallback((user: User) => { + setSelectedUser(user); + setEditModalOpen(true); + }, []); + + const handleEditClose = useCallback(() => { + setEditModalOpen(false); + setSelectedUser(null); + }, []); + + const handleEditSave = useCallback( + async (userId: string, userData: any) => { + try { + const result = await updateUser(userId, userData); + if (result.success) { + setSnackbar({ + open: true, + message: "Customer updated successfully!", + severity: "success", + }); + return { success: true }; + } else { + return { success: false, error: result.error }; + } + } catch (error) { + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to update customer", + }; + } + }, + [updateUser], + ); + + const handleDeleteClick = useCallback((userId: string) => { + setUserToDelete(userId); + setDeleteDialogOpen(true); + }, []); + + const handleDeleteConfirm = useCallback(async () => { + if (!userToDelete) return; + + try { + const result = await deleteUser(userToDelete); + if (result.success) { + setSnackbar({ + open: true, + message: "Customer deleted successfully!", + severity: "success", + }); + } else { + setSnackbar({ + open: true, + message: result.error || "Failed to delete customer", + severity: "error", + }); + } + } catch (error) { + setSnackbar({ + open: true, + message: "Failed to delete customer", + severity: "error", + }); + } finally { + setDeleteDialogOpen(false); + setUserToDelete(null); + } + }, [deleteUser, userToDelete]); + + const handleDeleteCancel = useCallback(() => { + setDeleteDialogOpen(false); + setUserToDelete(null); + }, []); + + const handleSnackbarClose = useCallback(() => { + setSnackbar((prev) => ({ ...prev, open: false })); + }, []); + + return ( + + {/* Header */} + + + Customer Management + + + Manage your customer database with search, edit, and delete + capabilities. + + + + {/* Error Alert */} + {error && ( + + {error} + + )} + + {/* Data Grid */} + + + {/* Edit Modal */} + + + {/* Delete Confirmation Dialog */} + + Confirm Delete + + + Are you sure you want to delete this customer? This action cannot be + undone. + + + + + + + + + {/* Success/Error Snackbar */} + + + {snackbar.message} + + + + ); +} diff --git a/src/crm/pages/Deals.tsx b/src/crm/pages/Deals.tsx new file mode 100644 index 0000000..1aa0eac --- /dev/null +++ b/src/crm/pages/Deals.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +export default function Deals() { + return ( + + + Deals Page + + + This is the deals management page where you can track and manage your + sales pipeline. + + + ); +} diff --git a/src/crm/pages/Reports.tsx b/src/crm/pages/Reports.tsx new file mode 100644 index 0000000..85682cc --- /dev/null +++ b/src/crm/pages/Reports.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +export default function Reports() { + return ( + + + Reports Page + + + This is the reports page where you can access and generate various + analytics and insights. + + + ); +} diff --git a/src/crm/pages/Settings.tsx b/src/crm/pages/Settings.tsx new file mode 100644 index 0000000..5ccf1a6 --- /dev/null +++ b/src/crm/pages/Settings.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +export default function Settings() { + return ( + + + Settings Page + + + This is the settings page where you can configure your CRM preferences + and manage your account. + + + ); +} diff --git a/src/crm/pages/Tasks.tsx b/src/crm/pages/Tasks.tsx new file mode 100644 index 0000000..3f4971d --- /dev/null +++ b/src/crm/pages/Tasks.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +export default function Tasks() { + return ( + + + Tasks Page + + + This is the tasks management page where you can track all your + activities and follow-ups. + + + ); +} diff --git a/src/crm/types/user.ts b/src/crm/types/user.ts new file mode 100644 index 0000000..4b85d63 --- /dev/null +++ b/src/crm/types/user.ts @@ -0,0 +1,79 @@ +export interface User { + login: { + uuid: string; + username: string; + password: string; + }; + name: { + title: string; + first: string; + last: string; + }; + gender: string; + location: { + street: { + number: number; + name: string; + }; + city: string; + state: string; + country: string; + postcode: string; + coordinates: { + latitude: number; + longitude: number; + }; + timezone: { + offset: string; + description: string; + }; + }; + email: string; + dob: { + date: string; + age: number; + }; + registered: { + date: string; + age: number; + }; + phone: string; + cell: string; + picture: { + large: string; + medium: string; + thumbnail: string; + }; + nat: string; +} + +export interface UserFormData { + email: string; + name: { + title: string; + first: string; + last: string; + }; + gender: string; + location: { + street: { + number: number; + name: string; + }; + city: string; + state: string; + country: string; + postcode: string; + }; + phone: string; + cell: string; +} + +export interface UsersApiResponse { + page: number; + perPage: number; + total: number; + span: string; + effectivePage: number; + data: User[]; +} diff --git a/src/shared-theme/AppTheme.tsx b/src/shared-theme/AppTheme.tsx index b8e5b3a..e7b51c4 100644 --- a/src/shared-theme/AppTheme.tsx +++ b/src/shared-theme/AppTheme.tsx @@ -28,6 +28,7 @@ export default function AppTheme(props: AppThemeProps) { colorSchemeSelector: "data-mui-color-scheme", cssVarPrefix: "template", }, + defaultColorScheme: "light", // Set light mode as default instead of using system preference colorSchemes, // Recently added in v6 for building light & dark mode app, see https://mui.com/material-ui/customization/palette/#color-schemes typography, shadows,