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/CustomerModal.tsx b/src/crm/components/CustomerModal.tsx new file mode 100644 index 0000000..ec72f50 --- /dev/null +++ b/src/crm/components/CustomerModal.tsx @@ -0,0 +1,399 @@ +import * as React from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Grid, + FormControl, + InputLabel, + Select, + MenuItem, + Box, + CircularProgress, + Alert, +} from "@mui/material"; +import { + User, + CreateUserRequest, + UpdateUserRequest, +} from "../services/usersApi"; + +interface CustomerModalProps { + open: boolean; + onClose: () => void; + onSave: (userData: CreateUserRequest | UpdateUserRequest) => Promise; + user?: User | null; + loading?: boolean; +} + +const initialFormData = { + email: "", + username: "", + password: "", + firstName: "", + lastName: "", + title: "Mr", + gender: "male", + phone: "", + cell: "", + streetNumber: "", + streetName: "", + city: "", + state: "", + country: "", + postcode: "", +}; + +export default function CustomerModal({ + open, + onClose, + onSave, + user, + loading = false, +}: CustomerModalProps) { + const [formData, setFormData] = React.useState(initialFormData); + const [error, setError] = React.useState(null); + const [isSubmitting, setIsSubmitting] = React.useState(false); + + const isEditMode = Boolean(user); + + React.useEffect(() => { + if (user) { + setFormData({ + email: user.email || "", + username: user.login.username || "", + password: "", + firstName: user.name.first || "", + lastName: user.name.last || "", + title: user.name.title || "Mr", + gender: user.gender || "male", + phone: user.phone || "", + cell: user.cell || "", + streetNumber: user.location?.street?.number?.toString() || "", + streetName: user.location?.street?.name || "", + city: user.location?.city || "", + state: user.location?.state || "", + country: user.location?.country || "", + postcode: user.location?.postcode || "", + }); + } else { + setFormData(initialFormData); + } + setError(null); + }, [user, open]); + + const handleInputChange = + (field: string) => + ( + event: + | React.ChangeEvent + | { target: { value: unknown } }, + ) => { + setFormData((prev) => ({ + ...prev, + [field]: event.target.value as string, + })); + setError(null); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setError(null); + setIsSubmitting(true); + + try { + if (isEditMode) { + const updateData: UpdateUserRequest = { + email: formData.email, + name: { + first: formData.firstName, + last: formData.lastName, + title: formData.title, + }, + gender: formData.gender, + phone: formData.phone, + cell: formData.cell, + location: { + street: { + number: parseInt(formData.streetNumber) || 0, + name: formData.streetName, + }, + city: formData.city, + state: formData.state, + country: formData.country, + postcode: formData.postcode, + }, + }; + await onSave(updateData); + } else { + const createData: CreateUserRequest = { + email: formData.email, + login: { + username: formData.username, + password: formData.password, + }, + name: { + first: formData.firstName, + last: formData.lastName, + title: formData.title, + }, + gender: formData.gender, + location: { + street: { + number: parseInt(formData.streetNumber) || 0, + name: formData.streetName, + }, + city: formData.city, + state: formData.state, + country: formData.country, + postcode: formData.postcode, + }, + }; + await onSave(createData); + } + onClose(); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + } finally { + setIsSubmitting(false); + } + }; + + const handleClose = () => { + if (!isSubmitting) { + onClose(); + } + }; + + return ( + +
+ + {isEditMode ? "Edit Customer" : "Add New Customer"} + + + {error && ( + + {error} + + )} + + {/* Basic Information */} + + + Basic Information + + + + + + {!isEditMode && ( + <> + + + + + + + + )} + + {/* Name Information */} + + + Title + + + + + + + + + + + + Gender + + + + + {/* Contact Information */} + + + Contact Information + + + + + + + + + + {/* Address Information */} + + + Address Information + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/src/crm/components/DeleteCustomerDialog.tsx b/src/crm/components/DeleteCustomerDialog.tsx new file mode 100644 index 0000000..26f5603 --- /dev/null +++ b/src/crm/components/DeleteCustomerDialog.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + CircularProgress, +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { User } from "../services/usersApi"; + +interface DeleteCustomerDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; + user: User | null; + loading?: boolean; +} + +export default function DeleteCustomerDialog({ + open, + onClose, + onConfirm, + user, + loading = false, +}: DeleteCustomerDialogProps) { + return ( + + Confirm Delete + + Are you sure you want to delete {user?.name.first} {user?.name.last}? + This action cannot be undone. + + + + + + + ); +} 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..9a8ef3d --- /dev/null +++ b/src/crm/pages/Customers.tsx @@ -0,0 +1,481 @@ +import * as React from "react"; +import { + Box, + Typography, + Button, + TextField, + Card, + CardContent, + Stack, + Alert, + Snackbar, + Avatar, + Chip, + IconButton, + Menu, + MenuItem, + ListItemIcon, + ListItemText, +} from "@mui/material"; +import { + DataGrid, + GridColDef, + GridRowParams, + GridToolbar, +} from "@mui/x-data-grid"; +import AddIcon from "@mui/icons-material/Add"; +import SearchIcon from "@mui/icons-material/Search"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import EditIcon from "@mui/icons-material/Edit"; + +import PersonIcon from "@mui/icons-material/Person"; +import { + usersApi, + User, + CreateUserRequest, + UpdateUserRequest, +} from "../services/usersApi"; +import CustomerModal from "../components/CustomerModal"; +import DeleteCustomerDialog from "../components/DeleteCustomerDialog"; + +interface ActionMenuProps { + user: User; + onEdit: (user: User) => void; + onDelete: (user: User) => void; +} + +function ActionMenu({ user, onEdit, onDelete }: ActionMenuProps) { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + event.stopPropagation(); + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleEdit = () => { + onEdit(user); + handleClose(); + }; + + const handleDelete = () => { + onDelete(user); + handleClose(); + }; + + return ( + <> + + + + + + + + + Edit + + + + + + Delete + + + + ); +} + +export default function Customers() { + const [users, setUsers] = React.useState([]); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + const [searchQuery, setSearchQuery] = React.useState(""); + const [page, setPage] = React.useState(0); + const [pageSize, setPageSize] = React.useState(25); + const [totalRows, setTotalRows] = React.useState(0); + + // Modal states + const [modalOpen, setModalOpen] = React.useState(false); + const [selectedUser, setSelectedUser] = React.useState(null); + const [modalLoading, setModalLoading] = React.useState(false); + + // Delete confirmation states + const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); + const [userToDelete, setUserToDelete] = React.useState(null); + const [deleting, setDeleting] = React.useState(false); + + // Snackbar states + const [snackbar, setSnackbar] = React.useState<{ + open: boolean; + message: string; + severity: "success" | "error"; + }>({ + open: false, + message: "", + severity: "success", + }); + + const showSnackbar = ( + message: string, + severity: "success" | "error" = "success", + ) => { + setSnackbar({ open: true, message, severity }); + }; + + const closeSnackbar = () => { + setSnackbar((prev) => ({ ...prev, open: false })); + }; + + const fetchUsers = React.useCallback(async () => { + setLoading(true); + setError(null); + + try { + const response = await usersApi.getUsers({ + page: page + 1, // API uses 1-based pagination + perPage: pageSize, + search: searchQuery, + sortBy: "name.first", + }); + + setUsers(response.data); + setTotalRows(response.total); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch users"); + } finally { + setLoading(false); + } + }, [page, pageSize, searchQuery]); + + React.useEffect(() => { + fetchUsers(); + }, [fetchUsers]); + + // Debounced search + const debouncedSearch = React.useMemo(() => { + const timer = setTimeout(() => { + setPage(0); // Reset to first page when searching + }, 300); + return () => clearTimeout(timer); + }, [searchQuery]); + + const handleSearchChange = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + }; + + const handleAddUser = () => { + setSelectedUser(null); + setModalOpen(true); + }; + + const handleEditUser = (user: User) => { + setSelectedUser(user); + setModalOpen(true); + }; + + const handleDeleteUser = (user: User) => { + setUserToDelete(user); + setDeleteDialogOpen(true); + }; + + const confirmDelete = async () => { + if (!userToDelete) return; + + setDeleting(true); + try { + await usersApi.deleteUser(userToDelete.login.uuid); + showSnackbar("User deleted successfully"); + fetchUsers(); + } catch (err) { + showSnackbar( + err instanceof Error ? err.message : "Failed to delete user", + "error", + ); + } finally { + setDeleting(false); + setDeleteDialogOpen(false); + setUserToDelete(null); + } + }; + + const handleSaveUser = async ( + userData: CreateUserRequest | UpdateUserRequest, + ) => { + setModalLoading(true); + try { + if (selectedUser) { + // Update existing user + await usersApi.updateUser( + selectedUser.login.uuid, + userData as UpdateUserRequest, + ); + showSnackbar("User updated successfully"); + } else { + // Create new user + await usersApi.createUser(userData as CreateUserRequest); + showSnackbar("User created successfully"); + } + fetchUsers(); + } catch (err) { + throw err; // Let the modal handle the error + } finally { + setModalLoading(false); + } + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString(); + }; + + const getInitials = (first: string, last: string) => { + return `${first.charAt(0)}${last.charAt(0)}`.toUpperCase(); + }; + + const columns: GridColDef[] = [ + { + field: "avatar", + headerName: "", + width: 60, + sortable: false, + filterable: false, + align: "center", + headerAlign: "center", + renderCell: (params) => { + const user = params.row as User; + return ( + + {user.picture?.thumbnail ? ( + {`${user.name.first} + ) : ( + getInitials(user.name.first, user.name.last) + )} + + ); + }, + }, + { + field: "name", + headerName: "Name", + width: 200, + valueGetter: (value, row: User) => + `${row.name.title} ${row.name.first} ${row.name.last}`, + }, + { + field: "email", + headerName: "Email", + width: 250, + valueGetter: (value, row: User) => row.email, + }, + { + field: "username", + headerName: "Username", + width: 150, + valueGetter: (value, row: User) => row.login.username, + }, + { + field: "location", + headerName: "Location", + width: 200, + valueGetter: (value, row: User) => + `${row.location.city}, ${row.location.country}`, + }, + { + field: "phone", + headerName: "Phone", + width: 150, + valueGetter: (value, row: User) => row.phone, + }, + { + field: "gender", + headerName: "Gender", + width: 100, + renderCell: (params) => { + const user = params.row as User; + return ( + + ); + }, + }, + { + field: "age", + headerName: "Age", + width: 80, + valueGetter: (value, row: User) => row.dob.age, + }, + { + field: "registered", + headerName: "Registered", + width: 120, + valueGetter: (value, row: User) => formatDate(row.registered.date), + }, + { + field: "actions", + headerName: "Actions", + width: 80, + sortable: false, + filterable: false, + renderCell: (params) => { + const user = params.row as User; + return ( + + ); + }, + }, + ]; + + return ( + + + Customer Management + + + + + + Customers + + + ), + }} + sx={{ minWidth: 200 }} + /> + + + + + {error && ( + + {error} + + )} + + + setPage(newPage)} + onPageSizeChange={(newPageSize) => setPageSize(newPageSize)} + pageSizeOptions={[10, 25, 50, 100]} + loading={loading} + getRowId={(row) => row.login.uuid} + density="comfortable" + disableRowSelectionOnClick + rowHeight={56} + slots={{ + toolbar: GridToolbar, + }} + slotProps={{ + toolbar: { + showQuickFilter: false, + }, + }} + sx={{ + "& .MuiDataGrid-cell": { + display: "flex", + alignItems: "center", + justifyContent: "flex-start", + }, + '& .MuiDataGrid-cell[data-field="avatar"]': { + justifyContent: "center", + }, + }} + /> + + + + + {/* Add/Edit Customer Modal */} + setModalOpen(false)} + onSave={handleSaveUser} + user={selectedUser} + loading={modalLoading} + /> + + {/* Delete Confirmation Dialog */} + setDeleteDialogOpen(false)} + onConfirm={confirmDelete} + user={userToDelete} + loading={deleting} + /> + + {/* 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/services/usersApi.ts b/src/crm/services/usersApi.ts new file mode 100644 index 0000000..ce4c1e3 --- /dev/null +++ b/src/crm/services/usersApi.ts @@ -0,0 +1,197 @@ +// Types for User API +export interface UserLocation { + street: { + number: number; + name: string; + }; + city: string; + state: string; + country: string; + postcode: string; + coordinates: { + latitude: number; + longitude: number; + }; + timezone: { + offset: string; + description: string; + }; +} + +export interface UserName { + title: string; + first: string; + last: string; +} + +export interface UserLogin { + uuid: string; + username: string; + password: string; +} + +export interface UserPicture { + large: string; + medium: string; + thumbnail: string; +} + +export interface User { + login: UserLogin; + name: UserName; + gender: string; + location: UserLocation; + email: string; + dob: { + date: string; + age: number; + }; + registered: { + date: string; + age: number; + }; + phone: string; + cell: string; + picture: UserPicture; + nat: string; +} + +export interface UsersApiResponse { + page: number; + perPage: number; + total: number; + span: string; + effectivePage: number; + data: User[]; +} + +export interface CreateUserRequest { + email: string; + login: { + username: string; + password?: string; + }; + name: { + first: string; + last: string; + title?: string; + }; + gender?: string; + location?: Partial; +} + +export interface UpdateUserRequest { + email?: string; + name?: Partial; + location?: Partial; + phone?: string; + cell?: string; + gender?: string; +} + +const BASE_URL = "https://user-api.builder-io.workers.dev/api"; + +export class UsersApiService { + async getUsers( + params: { + page?: number; + perPage?: number; + search?: string; + sortBy?: string; + span?: string; + } = {}, + ): Promise { + const queryParams = new URLSearchParams(); + + if (params.page) queryParams.append("page", params.page.toString()); + if (params.perPage) + queryParams.append("perPage", params.perPage.toString()); + if (params.search) queryParams.append("search", params.search); + if (params.sortBy) queryParams.append("sortBy", params.sortBy); + if (params.span) queryParams.append("span", params.span); + + const response = await fetch(`${BASE_URL}/users?${queryParams}`); + + if (!response.ok) { + throw new Error(`Failed to fetch users: ${response.statusText}`); + } + + return response.json(); + } + + async getUserById(id: string): Promise { + const response = await fetch(`${BASE_URL}/users/${encodeURIComponent(id)}`); + + if (!response.ok) { + throw new Error(`Failed to fetch user: ${response.statusText}`); + } + + return response.json(); + } + + async createUser( + userData: CreateUserRequest, + ): Promise<{ success: boolean; uuid: string; message: string }> { + const response = await fetch(`${BASE_URL}/users`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error( + error.error || `Failed to create user: ${response.statusText}`, + ); + } + + return response.json(); + } + + async updateUser( + id: string, + userData: UpdateUserRequest, + ): Promise<{ success: boolean; message: string }> { + const response = await fetch( + `${BASE_URL}/users/${encodeURIComponent(id)}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }, + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error( + error.error || `Failed to update user: ${response.statusText}`, + ); + } + + return response.json(); + } + + async deleteUser(id: string): Promise<{ success: boolean; message: string }> { + const response = await fetch( + `${BASE_URL}/users/${encodeURIComponent(id)}`, + { + method: "DELETE", + }, + ); + + if (!response.ok) { + const error = await response.json(); + throw new Error( + error.error || `Failed to delete user: ${response.statusText}`, + ); + } + + return response.json(); + } +} + +export const usersApi = new UsersApiService(); 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,