diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..0643ee0 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "extends": [ + "next/core-web-vitals", + "plugin:security/recommended-legacy" + ], + "plugins": [ + "security" + ] +} \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..05a93e5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + minor-and-patch: + update-types: ["minor", "patch"] + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..935d566 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,64 @@ +name: CI + +on: + pull_request: + branches: [main, stag] + push: + branches: [main, stag] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install Dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Type Check + run: npm run typecheck + continue-on-error: true + + - name: Write Format + run: npm run format:write + + - name: Check Format + run: npm run format:check + + - name: Build + run: npx vercel build --yes + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + MONGODB_URI: ${{ secrets.MONGODB_URI }} + NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} + NEXTAUTH_URL: ${{ secrets.NEXTAUTH_URL }} + NEXT_PUBLIC_SALESFAM_API_KEY: ${{ secrets.NEXT_PUBLIC_SALESFAM_API_KEY }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} + + secrets-scan: + runs-on: ubuntu-latest + container: zricethezav/gitleaks:latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Escanear secretos + run: gitleaks detect --source=. --verbose --redact diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0523389 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.env +.env*.local +node_modules/ +.next/ +out/ +coverage/ +.vercel +*.tsbuildinfo +npm-debug.log* +.DS_Store diff --git a/README.md b/README.md index a6731ff..7caba76 100644 --- a/README.md +++ b/README.md @@ -1 +1,129 @@ -#SALESFAM SALESFAM +# Sales Fam + +Unleash Your Sales Potential with Sales Fam — a Next.js 14 (App Router) CRM/sales platform for managing companies, clients, projects, invoices, sales meetings, commissions, and contracts. + +## Tech Stack + +- **Framework:** Next.js 14 (App Router), React 18, TypeScript +- **Database:** MongoDB via Mongoose +- **Auth:** NextAuth (Credentials provider + JWT sessions) +- **UI:** Tailwind CSS, Radix UI, shadcn/ui, lucide-react +- **File uploads:** Multer + Cloudinary +- **Email:** Resend +- **Forms/validation:** react-hook-form + zod + +## Prerequisites + +- **Node.js 20.x** (matches the CI pipeline in [.github/workflows/ci.yaml](.github/workflows/ci.yaml)) +- **npm** (comes with Node) +- A **MongoDB** connection string (a free [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) cluster works fine) +- A **Cloudinary** account (for avatar/image uploads) +- A **Resend** account (for transactional emails) — optional for basic local development, required for any feature that sends emails (invitations, notifications, password reset, etc.) + +## Getting Started + +### 1. Clone and install dependencies + +```bash +git clone +cd Salesfam_Org +npm install +``` + +### 2. Configure environment variables + +Create a `.env` file at the project root (it is already git-ignored). Copy the template below and fill in your own values: + +```bash +# --- MongoDB --- +# Connection string to your MongoDB cluster/database +MONGODB_URI=mongodb+srv://:@/?appName= + +# --- NextAuth --- +# Random secret used to sign/encrypt session tokens. +# Generate one with: openssl rand -base64 32 +NEXTAUTH_SECRET= +# Canonical URL of the app (required by NextAuth internally in some flows) +NEXTAUTH_URL=http://localhost:8080 +# Public URL exposed to the client (used to build invite/signup links) +NEXT_PUBLIC_NEXTAUTH_URL=http://localhost:8080 + +# --- App / API base URL --- +# Used server-side to build absolute URLs (e.g. for server components/fetches) +NEXT_PUBLIC_API_URL=http://localhost:8080 + +# --- Resend (transactional email) --- +RESEND_API_KEY= + +# --- Misc / app-specific --- +# Email treated as the platform admin/superadmin (used in project & contract flows) +NEXT_PUBLIC_ADMIN_EMAIL= +# API key used to identify/authorize the Sales Fam public API surface +NEXT_PUBLIC_SALESFAM_API_KEY= +``` + +> **Never commit `.env`.** It already contains real credentials pointing at a live database — treat it as a secret file, not a template. + +### 3. Run the app in development mode + +```bash +npm run dev +``` + +The dev server starts on **http://localhost:8080** (custom port set in [package.json](package.json)). + +### 4. Log in / create a user + +There is no seed script yet. To get a working account, either: +- Use the `/signup` page in the browser, or +- Insert a user document directly into the `users` collection in MongoDB (with a `bcrypt`-hashed `password` field, matching [models/user.js](models/user.js) and the credentials check in [app/api/auth/[...nextauth]/route.js](app/api/auth/%5B...nextauth%5D/route.js)). + +## Available npm Scripts + +| Script | Command | Description | +| --- | --- | --- | +| `npm run dev` | `next dev -p 8080` | Start the local dev server with hot reload on port 8080 | +| `npm run build` | `next build` | Create a production build | +| `npm run start` | `next start` | Serve the production build (run `build` first) | +| `npm run preview` | `next build && next start -p 8081` | Build and serve production output locally on port 8081 | +| `npm run lint` | `next lint` | Run ESLint | +| `npm run lint:fix` | `next lint --fix` | Run ESLint and auto-fix issues | +| `npm run typecheck` | `tsc --noEmit` | Run the TypeScript compiler in check-only mode | +| `npm run format:write` | `prettier --write "**/*.{ts,tsx,mdx}" --cache` | Format code with Prettier | +| `npm run format:check` | `prettier --check "**/*.{ts,tsx,mdx}" --cache` | Check formatting without writing changes | + +## Project Structure + +``` +app/ Next.js App Router pages and API routes (app/api/**) +components/ Reusable React components (incl. shadcn/ui primitives) +config/ Static site configuration (name, description, nav) +lib/ Server/client utilities: MongoDB connection, Cloudinary, email, data fetching +middleware.js Route protection based on NextAuth session cookie +models/ Mongoose schemas (client, company, invoice, project, salesMetting, user) +public/ Static assets +styles/ Global styles +types/ Shared TypeScript types +``` + +## Authentication & Route Protection + +[middleware.js](middleware.js) guards routes such as `/dashboard`, `/project`, `/settings`, `/sales`, `/api/**`, etc. Unauthenticated requests to protected routes are redirected to `/`; authenticated users hitting `/login` or `/signup` are redirected to `/dashboard`. Auth state is read from the `next-auth.session-token` / `__Secure-next-auth.session-token` cookie, so `NEXTAUTH_SECRET` must be set for sessions to validate correctly. + +## Working on Features / Fixes + +1. Create a branch from `main`. +2. Run `npm run dev` and reproduce/verify against `http://localhost:8080`. +3. Before opening a PR, run the same checks CI runs (see [.github/workflows/ci.yaml](.github/workflows/ci.yaml)): + ```bash + npm run lint + npm run typecheck + npm run format:check + ``` +4. Check [SECURITY_DEBT.md](SECURITY_DEBT.md) for known, accepted risks before "fixing" something that's already tracked there. +5. CI also runs a `gitleaks` secrets scan — never commit `.env` or any real credentials. + +## Notes + +- `next.config.mjs` currently sets `typescript.ignoreBuildErrors: true`, so `npm run build` will succeed even with type errors — always run `npm run typecheck` separately. +- Image uploads only work for remote hosts `img.youtube.com` and `res.cloudinary.com` (see `images.remotePatterns` in [next.config.mjs](next.config.mjs)); adding another image source requires updating that list. diff --git a/SECURITY_DEBT.md b/SECURITY_DEBT.md new file mode 100644 index 0000000..bb362b3 --- /dev/null +++ b/SECURITY_DEBT.md @@ -0,0 +1,26 @@ +# Known Security Debt + +## 🟢 Fixed + +### 1. Regex Injection / ReDoS in Company Search +- **File:** `app/api/company/route.js:120` +- **Risk:** `companyName` is received unsanitized from a query parameter and concatenated into a `RegExp`. This allows for ReDoS (server hang) and exact match bypass. +- **Suggested Fix:** Escape special regex characters before building the `RegExp`, or use an exact Mongo filter instead of `$regex`. +- **Detected by:** `eslint-plugin-security` (`detect-non-literal-regexp`), does not block CI (severity: warning). +- **Fix Applied:** Changed the previous regex to a Mongo filter and this way prevent Regex Injection and ReDoS + +### 2. List Elements Without `key` (5 instances) +- **Files:** `app/settings/manage-contracts/AddContract.tsx:144`, `EditContract.tsx:156`, `components/ContractTable.jsx:99`, `components/datatable.tsx:363`, `components/datatableSeller1.tsx:275` +- **Risk:** Not a security issue — it is a React bug that can cause incorrect rendering when reordering/updating lists. +- **Suggested Fix:** Add `key={unique-id}` to each iterated element. +- **Note:** Temporarily downgraded to a warning via `eslint-disable-next-line` to avoid blocking the pipeline — see comments in each file. +- **Fix Applied:** Added a unique id key to each of the iterated element + +## 🟢 Reviewed — False Positive, No Action Required + +- `components/ResetPassword.tsx:29`, `components/SetPassword.tsx:34` — Comparison between two fields of the same form, not between a secret and a stored value. No remote attacker can measure timing here. +- `components/TabComponent.tsx:23` — The index used never comes from user input, only from the `tabs` array itself rendered by the component. + +## Recommended run 'npm run lint' to see other needs + +- `grep -rn "eslint-disable" --include="*.tsx" --include="*.jsx" --include="*.js" .` run it eventually to check not documented bypassed lines. \ No newline at end of file diff --git a/app/Providers.js b/app/Providers.js new file mode 100644 index 0000000..1c82af6 --- /dev/null +++ b/app/Providers.js @@ -0,0 +1,7 @@ +"use client" + +import { SessionProvider } from "next-auth/react" + +export const AuthProvider = ({ children }) => { + return {children} +} diff --git a/app/add-project/AddProjectClient.tsx b/app/add-project/AddProjectClient.tsx new file mode 100644 index 0000000..78dcf6b --- /dev/null +++ b/app/add-project/AddProjectClient.tsx @@ -0,0 +1,640 @@ +"use client" + +import { useEffect, useState } from "react" +import * as React from "react" +import Link from "next/link" +import { useRouter } from "next/navigation" +import { format } from "date-fns" +import { + ArrowLeftIcon, + Calendar as CalendarIcon, + Check, + ChevronsUpDown, +} from "lucide-react" +import { useSession } from "next-auth/react" + +import { fetchCompanies } from "@/lib/company/company" +import { fetchClients } from "@/lib/fetchClients" +import { getUser } from "@/lib/getUser" +import { + sendAdminNotification, + sendUserNotification, +} from "@/lib/notification/sendNotification" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Calendar } from "@/components/ui/calendar" +import { Checkbox } from "@/components/ui/checkbox" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +export default function AddProject() { + const AdminEmail = process.env.NEXT_PUBLIC_ADMIN_EMAIL + const [open, setOpen] = useState(false) + const [openClient, setOpenClient] = useState(false) + const [value, setValue] = useState("") + const [valueClient, setValueClient] = useState("") + + const { toast } = useToast() + const router = useRouter() + + const { data: session } = useSession() + + const [isLoading, setLoading] = useState(false) + const [projectName, setprojectName] = useState("") + const [salesPerson, setUserId] = useState("") + const [salesPersonEmail, setSalesPersonEmail] = useState("") + const [salesId, setsalesId] = useState() + const [clientId, setClientId] = useState("") + const [upSellerId, setupSellerId] = useState() + const [projectDetails, setprojectDetails] = useState("") + const [budget, setbudget] = useState("") + const [companyName, setcompanyName] = useState("image appeal") + let [dateSigned, setdateSigned] = useState("") + const [clientName, setclientName] = useState("") + const [email, setemail] = useState("") + const [phone, setphone] = useState("") + const [address, setaddress] = useState("") + const [Users, setUsers] = useState([]) + const [clients, setClients] = useState([]) + const [Allcompanies, setAllcompanies] = useState() + const [contracts, setContracts] = useState() + const [currentUser, setcurrentUser] = useState() + const [isClientCall, setIsClientCall] = useState(false) + const [isClientEmail, setIsClientEmail] = useState(false) + const [hasClient, setHasClient] = useState(false) + const role = session?.user?.role + const name = session?.user?.name + const id = session?.user?.id + const salesPersonsEmail = session?.user?.email + console.log(salesPersonsEmail) + + useEffect(() => { + fetch("/api/user") + .then((response) => response.json()) + .then((data) => { + const main = data.users + const users = main.filter((item) => item.role != "SuperAdmin") + setUsers(users) + console.log(users) + }) + .catch((error) => { + console.error("Error:", error) + }) + + if (role != "SuperAdmin") { + setUserId(name) + + + } + + fetchCompanies().then((data) => { + setAllcompanies(data) + }) + + if (session) { + const userId = session?.user?.id + const role = session?.user?.role + + fetchClients(role).then((data) => { + if (role !== "SuperAdmin" && role !== "Admin-IA") { + setClients(data) + const filterPerson = data.filter((item) => userId == item.sellerId) + setClients(filterPerson) + } else { + setClients(data) + } + }) + + getUser(userId).then((salesPerson) => { + console.log(salesPerson) + setcurrentUser(salesPerson) + setContracts(salesPerson.contracts) + + if (role != "SuperAdmin" && role == "Sales1") { + setsalesId(salesPerson._id) + setupSellerId(salesPerson.upSellerId) + } + + if (role != "SuperAdmin" && role == "Sales2") { + setsalesId(salesPerson._id) + setupSellerId(salesPerson.upSellerId) + } + if (role != "SuperAdmin" && role == "Admin-IA") { + setsalesId(salesPerson._id) + setupSellerId(salesPerson.upSellerId) + } + }) + } + }, [session, salesPerson, role]) + + //=====================handle has client + const handleClient = () => { + setHasClient(!hasClient) + if (!hasClient) { + setClientId("") + } + } + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + + function formatDate(dateString) { + const options = { year: "numeric", month: "long", day: "numeric" } + return new Intl.DateTimeFormat("en-US", options).format( + new Date(dateString) + ) + } + + if (dateSigned) { + dateSigned = formatDate(dateSigned) + } + if (clientId) { + if ( + !projectName || + !projectDetails || + !budget || + !salesPerson || + !salesId || + // !upSellerId || + !companyName || + !dateSigned + ) { + toast({ + variant: "destructive", + title: "All fields required with client.", + }) + setLoading(false) + return + } + } else { + if ( + !projectName || + !projectDetails || + !budget || + !salesPerson || + !salesId || + !upSellerId || + !companyName || + !dateSigned || + !clientName || + !email || + !phone || + !address + ) { + toast({ + variant: "destructive", + title: "All fields required.", + }) + setLoading(false) + return + } + } + + try { + const res = await fetch("api/project", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + projectName, + projectDetails, + budget, + salesPerson, + salesId, + upSellerId, + companyName: companyName ? companyName : null, + dateSigned, + clientName, + email: email ? email : null, + phone: phone ? phone : null, + address: address ? address : null, + clientId: clientId ? clientId : null, + callClient: isClientCall, + emailClient: isClientEmail, + }), + }) + + if (res.ok) { + setLoading(false) + await sendUserNotification(projectName, salesPerson, salesPersonEmail?salesPersonEmail:salesPersonsEmail) + await sendAdminNotification(projectName, salesPerson, AdminEmail) + router.push("/dashboard") + + } else { + if (res.status == 409) { + toast({ + variant: "destructive", + title: "Client email already exist!", + }) + } else if (res.status == 408) { + toast({ + variant: "destructive", + title: "Client name already exist!", + }) + } else { + toast({ + variant: "destructive", + title: "project submit failed!", + }) + } + setLoading(false) + } + } catch (error) { + console.log("Error during project submit:", error) + toast({ + variant: "destructive", + title: `Error during project submit:", ${error}`, + }) + setLoading(false) + } + } + + return ( +
+
+
+

+ +
+ +
+ + Add New Project +

+
+
+
+ + setprojectName(e.target.value.trim())} + placeholder="Project Name" + /> +
+
+ + + {(Users && role == "SuperAdmin") || + (Users && role == "Admin-IA") ? ( +
+ + + + + + + + No sales person found. + + {Users?.map((user) => ( + + { + setValue( + currentValue == value ? "" : currentValue + ) + setsalesId(user?._id) + setupSellerId(user?.upSellerId) + setUserId( + currentValue == value ? "" : currentValue + ) + setSalesPersonEmail(user.email) + setOpen(false) + }} + > + + + {user?.name} + + ))} + + + + +
+ ) : ( + <> + setUserId(e.target.value.trim())} + defaultValue={name} + /> + + )} +
+
+
+
+ + +
+
+ + + + + + + + + +
+
+ + setbudget(e.target.value)} + placeholder="Project Budget" + /> +
+
+ + + + )} +
+ {!hasClient && ( +
+ setIsClientCall(!isClientCall)} + /> + + setIsClientEmail(!isClientEmail)} + /> + +
+ )} +
+ +
+
+
+
+ ) +} diff --git a/app/add-project/page.tsx b/app/add-project/page.tsx new file mode 100644 index 0000000..aae52db --- /dev/null +++ b/app/add-project/page.tsx @@ -0,0 +1,8 @@ +// app/add-project/page.tsx +import AddProjectClient from "./AddProjectClient" + +export const dynamic = 'force-dynamic'; + +export default function AddProjectPage() { + return +} \ No newline at end of file diff --git a/app/api/auth/[...nextauth]/route.js b/app/api/auth/[...nextauth]/route.js new file mode 100644 index 0000000..3ec5048 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.js @@ -0,0 +1,88 @@ +import User from "@/models/user" +import bcrypt from "bcryptjs" +import NextAuth from "next-auth/next" +import CredentialsProvider from "next-auth/providers/credentials" +import { connectMongoDB } from "@/lib/mongodb" +export const authOptions = { + providers: [ + CredentialsProvider({ + name: "credentials", + credentials: {}, + + async authorize(credentials, req) { + const { email, password } = credentials + + try { + await connectMongoDB() + const user = await User.findOne({ email }) + + if (!user) { + return null + } + + const passwordsMatch = await bcrypt.compare(password, user.password) + + if (!passwordsMatch) { + return null + } + + return { + email: user.email, + name: user.name, + role: user.role, + commission_rate: user.commission_rate, + avatar: user.avatar, + id: user._id.toString(), + } + } catch (error) { + console.log("Error: ", error) + throw error // Rethrow the error to be caught by NextAuth.js + } + }, + }), + ], + callbacks: { + async jwt({ token, user, session, trigger }) { + if (trigger === "update" && session) { + return { + ...token, + name: session?.user.name, + } + } + if (user) { + return { + ...token, + role: user.role, + id: user.id, + commission_rate: user.commission_rate, + avatar: user.avatar, + } + } + return token + }, + + async session({ session, token }) { + return { + ...session, + user: { + ...session.user, + role: token.role, + id: token.id, // Use the id from the token + commission_rate: token.commission_rate, + avatar: token.avatar, + }, + } + }, + }, + session: { + strategy: "jwt", + }, + secret: process.env.NEXTAUTH_SECRET, + pages: { + signIn: "/", // Update this to your sign-in page + }, +} + +const handler = NextAuth(authOptions) + +export { handler as GET, handler as POST } diff --git a/app/api/client/route.js b/app/api/client/route.js new file mode 100644 index 0000000..258caf3 --- /dev/null +++ b/app/api/client/route.js @@ -0,0 +1,128 @@ +import { NextResponse } from "next/server" +import Client from "@/models/client" +import { getServerSession } from "next-auth" + +import { connectMongoDB } from "@/lib/mongodb" + +export async function POST(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + try { + const { salesPerson, clientName, email, phone, address } = + await req.json() + + await connectMongoDB() + const clientNameTrim =clientName?.trim() + await Client.create({ + sellerId: salesPerson, + clientName:clientNameTrim, + email, + phone, + address, + }) + + return NextResponse.json({ message: "Client Submited" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while Client Submited." }, + { status: 500 } + ) + } + } +} +export async function PUT(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const clientId = req.nextUrl.searchParams.get("id") + try { + await connectMongoDB() + const { clientName, email, phone, address, callClient, emailClient } = + await req.json() + + await Client.findByIdAndUpdate( + { _id: clientId }, + { + clientName, + email, + phone, + address, + callClient, + emailClient, + } + ) + + return NextResponse.json({ message: "Client updated" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while Client updated." }, + { status: 500 } + ) + } + } +} + +export async function GET(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const salesPerson = req.nextUrl.searchParams.get("salesperson") + const ClientId = req.nextUrl.searchParams.get("id") + const ClientName = req.nextUrl.searchParams.get("slug") + + if (!ClientId && salesPerson && !ClientName) { + try { + await connectMongoDB() + const client = await Client.find() + return NextResponse.json({ client }) + } catch (error) { + console.log(error) + } + } + if (ClientId && !salesPerson && !ClientName) { + try { + await connectMongoDB() + const client = await Client.findOne({ _id: ClientId }) + return NextResponse.json({ client }) + } catch (error) { + console.log(error) + } + } + if (ClientId && !salesPerson && !ClientName) { + try { + await connectMongoDB() + const client = await Client.findOne({ _id: ClientId }) + return NextResponse.json({ client }) + } catch (error) { + console.log(error) + } + } + if (!ClientId && !salesPerson && ClientName) { + try { + await connectMongoDB() + if (ClientName != null) { + const client = await Client.findOne({ ClientName }).select() + return NextResponse.json({ client }) + } + } catch (error) { + console.log(error) + } + } + } +} + +export async function DELETE(request) { + const session = await getServerSession(request) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const id = request.nextUrl.searchParams.get("id") + await connectMongoDB() + await Client.findByIdAndDelete(id) + return NextResponse.json({ message: "Client deleted" }, { status: 200 }) + } +} diff --git a/app/api/commission/route.js b/app/api/commission/route.js new file mode 100644 index 0000000..69b5e7e --- /dev/null +++ b/app/api/commission/route.js @@ -0,0 +1,115 @@ +import { NextResponse } from "next/server" +import Invoice from "@/models/invoice" +import User from "@/models/user" +import Project from "@/models/project" +import { getServerSession } from "next-auth" + +import { connectMongoDB } from "@/lib/mongodb" + +export async function GET(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + }else{ + const projectId = req.nextUrl.searchParams.get("id") + const upsellerId = req.nextUrl.searchParams.get("upsale") + const sellerId = req.nextUrl.searchParams.get("sellerId") + + if (projectId && !upsellerId && !sellerId) { + try { + await connectMongoDB() + const invoices = await Invoice.find({ + projectId, + commission_paid: { $ne: "No" }, + }).select() + + if (invoices.length > 0) { + const userId = invoices[0].userId + const user = await User.findOne({ _id: userId }) + if(user){ + const rate = user.commission_rate + + var totalEarnings = 0 + invoices.forEach(function (invoice) { + totalEarnings += invoice.amount + }) + + const commission = (totalEarnings * rate) / 100 + + return NextResponse.json({ commission }) + } + } else { + return NextResponse.json({ commission: 0 }) + } + } catch (error) { + console.error(error) + return NextResponse.json({ error: "An error occurred" }, { status: 500 }) + } + } + + if (!projectId && upsellerId && !sellerId) { + try { + await connectMongoDB() + const invoices = await Invoice.find({ + commission_paid: { $ne: "No" }, + }).select() + let commission=0 + let totalEarnings = 0 + const filterInvoice = invoices.filter((item)=>item.userId===upsellerId) + const filterUpSellerInvoice = invoices.filter((item)=>item.upsellerId===upsellerId) + if (filterInvoice.length > 0) { + for(const invoice of filterInvoice) { + const findProject= await Project.findById(invoice.projectId) + const commission_Rate = findProject.commisson_rate + totalEarnings +=invoice.amount*commission_Rate/100 + } + + } + + if(filterUpSellerInvoice.length>0){ + for(const invoice of filterUpSellerInvoice){ + const upsellerUser = await User.findById(invoice.userId) + if(upsellerUser){ + const upSellerPercentage = upsellerUser?.upsellerPercentage + commission+=invoice.amount*upSellerPercentage/100 + } + } + } + return NextResponse.json({ commission,totalEarnings }) + } catch (error) { + console.error(error) + return NextResponse.json({ commission: 0,totalEarnings:0 }) + } + } + + if (upsellerId && sellerId &&!projectId ) { + try { + await connectMongoDB() + const invoices = await Invoice.find({ + upsellerId, + userId: sellerId, + commission_paid: { $ne: "No" }, + }).select() + if (invoices.length > 0) { + const userIds = invoices[0].userId + if(userIds){ + const user = await User.findOne({ _id: userIds }) + let rate = user.upsellerPercentage + let totalEarnings = 0 + invoices.forEach(function (invoice) { + totalEarnings += invoice.amount + }) + const commission = (totalEarnings *Number(rate)) / 100 ||0 + return NextResponse.json({ commission}) + } + } else { + return NextResponse.json({ commission: 0 }) + } + } catch (error) { + console.error(error) + return NextResponse.json({ error: "An error occurred" }, { status: 500 }) + } + } + } + +} \ No newline at end of file diff --git a/app/api/company/route.js b/app/api/company/route.js new file mode 100644 index 0000000..d2d7f96 --- /dev/null +++ b/app/api/company/route.js @@ -0,0 +1,150 @@ +import { NextResponse } from "next/server" +import Company from "@/models/company" +import { getServerSession } from "next-auth" + +import { connectMongoDB } from "@/lib/mongodb" + +import { cloudDelete } from "../../../lib/cloudinary" + +export async function POST(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + try { + const { + companyLogo, + companyName, + companyAddress, + companyType, + companyEmail, + companyPhone, + overview, + rate, + socialLink, + } = await req.json() + await connectMongoDB() + const companyNameTrim = companyName?.trim() + await Company.create({ + companyLogo, + companyName: companyNameTrim, + companyAddress, + companyType, + companyEmail, + companyPhone, + overview, + rate, + socialLink, + }) + + return NextResponse.json({ message: "Company Submited" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while Company Submited." }, + { status: 500 } + ) + } + } +} +export async function PUT(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const projectId = req.nextUrl.searchParams.get("id") + try { + const { + companyLogo, + companyName, + companyAddress, + companyType, + companyEmail, + companyPhone, + overview, + rate, + socialLink, + } = await req.json() + + await connectMongoDB() + + const existingCompany = await Company.findById(projectId) + const companyNameTrim = companyName?.trim() + const updatedFields = { + companyName: companyNameTrim, + companyAddress, + companyType, + companyEmail, + companyPhone, + overview, + rate, + socialLink, + } + + if (companyLogo !== null) { + await cloudDelete(existingCompany.companyLogo) + updatedFields.companyLogo = companyLogo + } + + await Company.findByIdAndUpdate(projectId, updatedFields) + + return NextResponse.json({ message: "Company Edited!" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while Company Edit." }, + { status: 500 } + ) + } + } +} + +export async function GET(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const companyName = req.nextUrl.searchParams.get("companyName") + const _id = req.nextUrl.searchParams.get("id") + if (_id) { + try { + await connectMongoDB() + const company = await Company.findOne({ _id: _id }) + return NextResponse.json({ company }) + } catch (error) { + console.log(error) + } + } + if (companyName) { + try { + await connectMongoDB() + + const company = await Company.findOne({ + companyName: companyName }, + {}, + { collation: {locale: 'en', strength: 2}}).select() + + return NextResponse.json({ company }) + } catch (error) { + console.log(error) + } + } else { + try { + await connectMongoDB() + const company = await Company.find().select() + return NextResponse.json({ company }) + } catch (error) { + console.log(error) + } + } + } +} +export async function DELETE(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const companyName = req.nextUrl.searchParams.get("companyId") + await connectMongoDB() + await Company.findByIdAndDelete(companyName) + return NextResponse.json({ message: "Company deleted" }, { status: 200 }) + } +} diff --git a/app/api/dashboard/superadmin/route.js b/app/api/dashboard/superadmin/route.js new file mode 100644 index 0000000..1ce5f23 --- /dev/null +++ b/app/api/dashboard/superadmin/route.js @@ -0,0 +1,5 @@ +import { NextResponse } from "next/server" + +export async function GET(req) { + return NextResponse.json("hello") +} diff --git a/app/api/earnings/route.js b/app/api/earnings/route.js new file mode 100644 index 0000000..885c155 --- /dev/null +++ b/app/api/earnings/route.js @@ -0,0 +1,32 @@ +import { NextResponse } from "next/server" +import Invoice from "@/models/invoice" +import { getServerSession } from "next-auth" +import { connectMongoDB } from "@/lib/mongodb" + +export async function GET(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + }else{ + const projectId = req.nextUrl.searchParams.get("id") + + try { + await connectMongoDB() // Make sure this function is properly defined + const invoices = await Invoice.find({ + projectId, + status: { $ne: "Unpaid" }, + }).select() + + var totalEarnings = 0 + invoices.forEach(function (invoice) { + totalEarnings += invoice.amount + }) + + return NextResponse.json({ totalEarnings }) + } catch (error) { + console.error(error) // Log errors with console.error for better visibility + return NextResponse.json({ error: "An error occurred" }, { status: 500 }) + } + } + +} diff --git a/app/api/invite/route.js b/app/api/invite/route.js new file mode 100644 index 0000000..05616a7 --- /dev/null +++ b/app/api/invite/route.js @@ -0,0 +1,58 @@ +import { NextResponse } from "next/server" +import User from "@/models/user" +import bcrypt from "bcryptjs" +import { getServerSession } from "next-auth" + +import { connectMongoDB } from "@/lib/mongodb" + +export async function POST(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + try { + const { + name, + email, + upSeller, + upSellerId, + upsellerPercentage, + contracts: contractsDataFromPayload, + } = await req.json() + + const initialContractsData = [ + { + companyName: "WordSphere", + rate: 20 - upsellerPercentage, // Now upsellerPercentage is defined + logo: "/wordsphere.png", + companyType: "Web Development Agency", + }, + { + companyName: "image appeal", + rate: 20 - upsellerPercentage, // Now upsellerPercentage is defined + logo: "/ia.png", + companyType: "Graphics Design Agency", + }, + ] + + await connectMongoDB() + + await User.create({ + name, + email, + upSeller, + upSellerId, + upsellerPercentage, + contracts: contractsDataFromPayload || initialContractsData, + }) + + return NextResponse.json({ message: "User registered." }, { status: 201 }) + } catch (error) { + console.error("Error during user registration:", error) + return NextResponse.json( + { message: "An error occurred while registering the user." }, + { status: 500 } + ) + } + } +} diff --git a/app/api/invoice/route.js b/app/api/invoice/route.js new file mode 100644 index 0000000..e8fe79f --- /dev/null +++ b/app/api/invoice/route.js @@ -0,0 +1,279 @@ +import { NextResponse } from "next/server" +import Invoice from "@/models/invoice" +import Project from "@/models/project" +import User from "@/models/user" +import { getServerSession } from "next-auth" + +import { connectMongoDB } from "@/lib/mongodb" + +export async function POST(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + try { + const { projectId, userId, upsellerId, invoiceDate, amount, rate } = + await req.json() + await connectMongoDB() + await Invoice.create({ + projectId, + userId, + upsellerId: upsellerId || "none", + invoiceDate, + amount, + rate, + }) + + return NextResponse.json({ message: "Invoice Added" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while Invoice Submited." }, + { status: 500 } + ) + } + } +} + +export async function GET(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const projectId = req.nextUrl.searchParams.get("projectId") + const invoiceid = req.nextUrl.searchParams.get("invoiceid") + const userId = req.nextUrl.searchParams.get("userId") + const upsellerId = req.nextUrl.searchParams.get("upsellerId") + const companyName = req.nextUrl.searchParams.get("companyName") + if (!invoiceid && projectId && !userId && !upsellerId && !companyName) { + try { + // Establish the MongoDB connection before querying + await connectMongoDB() // Make sure this function is properly defined + + // Fetch all invoices with the same projectName + const invoices = await Invoice.find({ projectId }) + + return NextResponse.json({ invoices }) + } catch (error) { + console.error(error) // Log errors with console.error for better visibility + return NextResponse.json( + { error: "An error occurred" }, + { status: 500 } + ) + } + } + if (invoiceid && !projectId && !userId && !upsellerId && !companyName) { + try { + await connectMongoDB() // Make sure this function is properly defined + + // Fetch all invoices with the same projectName + const invoices = await Invoice.findOne({ _id: invoiceid }) + + return NextResponse.json({ invoices }) + } catch (error) { + console.error(error) // Log errors with console.error for better visibility + return NextResponse.json( + { error: "An error occurred" }, + { status: 500 } + ) + } + } + if (!invoiceid && !projectId && userId && !upsellerId && !companyName) { + try { + await connectMongoDB() // Make sure this function is properly defined + + // Fetch all invoices for the specified user + const invoices = await Invoice.find({ + userId, + commission_paid: { $ne: "No" }, + }).select() + + let total = 0 + // Calculate total amount based on invoices + for (const invoice of invoices) { + total += invoice.amount * (invoice.rate / 100) + } + return NextResponse.json({ total }) + } catch (error) { + console.error(error) // Log errors with console.error for better visibility + return NextResponse.json( + { error: "An error occurred" }, + { status: 500 } + ) + } + } + if (!invoiceid && !projectId && !userId && upsellerId && !companyName) { + try { + await connectMongoDB() // Make sure this function is properly defined + let commission =0; + // Fetch all invoices with the same projectName + const invoices = await Invoice.find({ + upsellerId, + commission_paid: { $ne: "No" }, + }).select() + if(invoices.length>0){ + for(const invoice of invoices){ + const findUser = await User.findById({_id:invoice.userId}) + if(findUser){ + const upSellerPercentage = findUser.upsellerPercentage||0 + commission+=invoice.amount*upSellerPercentage/100 + } + } + } + return NextResponse.json({ commission }) + } catch (error) { + console.error(error) // Log errors with console.error for better visibility + return NextResponse.json( + { error: "An error occurred" }, + { status: 500 } + ) + } + } + + if (!invoiceid && !projectId && !userId && !upsellerId && companyName) { + await connectMongoDB() + const invoices = await Invoice.find({ commission_paid: { $ne: "No" } }) + + if (invoices.length > 0) { + let totalEarning = 0 + for (const invoice of invoices) { + const invoiceProject = await Project.findOne({ + _id: invoice.projectId, + companyName: companyName, + }) + + if ( + invoiceProject && + invoice.projectId && + invoiceProject._id && + invoice.projectId.toString() === invoiceProject._id.toString() + ) { + const commissionRate = invoiceProject?.commisson_rate || 0 + totalEarning += + invoice.amount - (invoice.amount * commissionRate) / 100 + if (invoiceProject?.upSellerId !== "none") { + const projectUser = await User.findById({ + _id: invoiceProject.salesId, + }) + + const upsellerPercentage = + Number(projectUser.upsellerPercentage) || 0 + totalEarning -= (invoice.amount * upsellerPercentage) / 100 + } + } + } + return NextResponse.json({ totalEarning }) + } + + return NextResponse.json({ totalEarning: 0 }) + } + if (!invoiceid && !projectId && !userId && !upsellerId && !companyName) { + await connectMongoDB() + const invoices = await Invoice.find({ commission_paid: { $ne: "No" } }) + if (invoices.length > 0) { + let totalEarning = 0 + for (const invoice of invoices) { + const invoiceProject = await Project.findById({ + _id: invoice.projectId, + }) + if ( + invoiceProject && + invoice.projectId && + invoiceProject._id && + invoice.projectId.toString() === invoiceProject._id.toString() + ) { + const commissionRate = invoiceProject?.commisson_rate || 0 + totalEarning += + invoice.amount - (invoice.amount * commissionRate) / 100 + if ( + invoiceProject?.upSellerId !== "none" && + invoiceProject.salesId + ) { + const projectUser = await User.findById({ + _id: invoiceProject.salesId, + }) + if (projectUser) { + const upsellerPercentage = + Number(projectUser.upsellerPercentage) || 0 + totalEarning -= (invoice.amount * upsellerPercentage) / 100 + } + } + } + } + return NextResponse.json({ totalEarning }) + } + + return NextResponse.json({ totalEarning: 0 }) + } + } +} + +export async function PUT(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + try { + const id = req.nextUrl.searchParams.get("invoiceid") + const { + invoiceDate, + userId, + upsellerId, + amount, + status, + commission_paid, + } = await req.json() + await connectMongoDB() + + await Invoice.findByIdAndUpdate(id, { + invoiceDate, + userId, + upsellerId, + amount, + status, + commission_paid, + }) + + return NextResponse.json({ message: "Invoice Updated!" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while Invoice Submited." }, + { status: 500 } + ) + } + } +} + +export async function DELETE(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const id = req.nextUrl.searchParams.get("invoiceid") + const projectId = req.nextUrl.searchParams.get("projectid") + + if (id) { + await connectMongoDB() + await Invoice.findByIdAndDelete(id) + return NextResponse.json({ message: "Invoice Deleted" }, { status: 200 }) + } + + if (projectId) { + await connectMongoDB() + + // Use deleteMany to delete all documents with the same projectId + const deleteResult = await Invoice.deleteMany({ projectId }) + + if (deleteResult.deletedCount > 0) { + return NextResponse.json( + { message: `${deleteResult.deletedCount} Invoices Deleted` }, + { status: 200 } + ) + } else { + return NextResponse.json( + { message: "No Invoices found for deletion" }, + { status: 200 } + ) + } + } + } +} diff --git a/app/api/project/route.js b/app/api/project/route.js new file mode 100644 index 0000000..31df968 --- /dev/null +++ b/app/api/project/route.js @@ -0,0 +1,240 @@ +import { NextResponse } from "next/server" +import Client from "@/models/client" +import Project from "@/models/project" +import User from "@/models/user" +import Invoice from "@/models/invoice" +import { getServerSession } from "next-auth" + +import { connectMongoDB } from "@/lib/mongodb" + +export async function POST(req) { + try { + const session = await getServerSession(req) + if (!session) { + return new Response("Unauthorized", { status: 401 }) + } + + const { + projectName, + projectDetails, + budget, + companyName, + salesPerson, + upSellerId, + salesId, + dateSigned, + clientName, + email, + phone, + address, + clientId, + callClient, + emailClient, + } = await req.json() + + await connectMongoDB() + + const SalesCommission = await User.findById(salesId) + if (!SalesCommission) { + return new Response("Salesperson not found", { status: 404 }) + } + + const commissionRate = SalesCommission.contracts.find( + (item) => item.companyName === companyName + ) + if (!commissionRate) { + return new Response("Commission rate not found", { status: 404 }) + } + + let clientExist + + if (clientId) { + clientExist = await Client.findById(clientId) + } + + if (email && clientName) { + const findByEmail = await Client.findOne({ email }) + const findByName = await Client.findOne({ clientName }) + + if (findByEmail) { + return new Response("Email already exists", { status: 409 }) + } else if (findByName) { + return new Response("Name already exists", { status: 409 }) + } + } + const projectNameTrim =projectName?.trim() + const createdProject = await Project.create({ + projectName:projectNameTrim, + projectDetails, + budget, + companyName, + commisson_rate: commissionRate.rate, + salesPerson, + upSellerId, + salesId, + dateSigned, + clientName: clientExist ? clientExist.clientName : clientName, + email: clientExist ? clientExist.email : email, + phone: clientExist ? clientExist.phone : phone, + address: clientExist ? clientExist.address : address, + callClient, + emailClient, + }) + + if (createdProject && clientName && email && phone && address && salesId) { + await Client.create({ + clientName, + email, + phone, + address, + sellerId: salesId, + callClient, + emailClient, + }) + } + + return new Response("Project Submitted", { status: 201 }) + } catch (error) { + console.error("Error:", error) + return new Response("An error occurred while Project Submitted.", { + status: 500, + }) + } +} + +export async function PUT(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + try { + const id = req.nextUrl.searchParams.get("id") + + const { + projectName, + projectDetails, + budget, + companyName, + status, + salesPerson, + commisson_rate, + dateSigned, + clientName, + email, + phone, + address, + } = await req.json() + await connectMongoDB() + if (email) { + const updateClient = await Client.findOne({ email }) + if (updateClient) { + await Client.findByIdAndUpdate(updateClient._id, { + clientName, + email, + phone, + address, + }) + } + } + const projectNameTrim =projectName?.trim() + await Project.findByIdAndUpdate(id, { + projectName:projectNameTrim, + projectDetails, + budget, + companyName, + status, + salesPerson, + commisson_rate, + dateSigned, + clientName, + email, + phone, + address, + }) + + return NextResponse.json({ message: "Project Updated!" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while Project Submited." }, + { status: 500 } + ) + } + } +} + +export async function GET(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const salesPerson = req.nextUrl.searchParams.get("salesperson") + const projectId = req.nextUrl.searchParams.get("id") + const projectName = req.nextUrl.searchParams.get("slug") + if (projectId && !salesPerson && !projectName) { + try { + await connectMongoDB() + const project = await Project.findOne({ _id: projectId }) + return NextResponse.json({ project }) + } catch (error) { + console.log(error) + } + } + + if (!projectId && salesPerson && !projectName) { + try { + await connectMongoDB() + const project = await Project.find({ + salesPerson, + status: { $ne: "Pending" }, + }).select() + return NextResponse.json({ project }) + } catch (error) { + console.log(error) + } + } + if (!projectId && !salesPerson && projectName) { + try { + await connectMongoDB() + if (projectName != null) { + const project = await Project.findOne({ projectName }).select() + return NextResponse.json({ project }) + } else { + const projects = await Project.find() + return NextResponse.json({ projects }) + } + } catch (error) { + console.log(error) + } + } + if (!projectId && !salesPerson && !projectName) { + try { + await connectMongoDB() + if (projectName != null) { + const project = await Project.find().select() + return NextResponse.json({ project }) + } else { + const projects = await Project.find() + return NextResponse.json({ projects }) + } + } catch (error) { + console.log(error) + } + } + } +} + +export async function DELETE(request) { + const session = await getServerSession(request) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const id = request.nextUrl.searchParams.get("id") + await connectMongoDB() + const deleteProject= await Project.findByIdAndDelete(id) + if(deleteProject){ + await Invoice.deleteMany({projectId:deleteProject?._id}) + + } + return NextResponse.json({ message: "Project deleted" }, { status: 200 }) + } +} diff --git a/app/api/resetPassword/route.js b/app/api/resetPassword/route.js new file mode 100644 index 0000000..975971c --- /dev/null +++ b/app/api/resetPassword/route.js @@ -0,0 +1,27 @@ +import { NextResponse } from "next/server" +import User from "@/models/user" +import bcrypt from "bcryptjs" + +import { connectMongoDB } from "@/lib/mongodb" + +export async function PUT(req) { + try { + const { email, password } = await req.json() + console.log(email,password) + const userIdentifier = { email: email } + const hashedPassword = await bcrypt.hash(password, 10) + await connectMongoDB() + + // Update user's password + await User.findOneAndUpdate(userIdentifier, { + password: hashedPassword, + }) + + return NextResponse.json({ message: "Password Updated!" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while resetting your password." }, + { status: 500 } + ) + } +} diff --git a/app/api/salesmetting/route.js b/app/api/salesmetting/route.js new file mode 100644 index 0000000..450b14b --- /dev/null +++ b/app/api/salesmetting/route.js @@ -0,0 +1,59 @@ +import { NextResponse } from "next/server" +import { getServerSession } from "next-auth" + +import { connectMongoDB } from "@/lib/mongodb" + +import salesMeeting from "../../../models/salesMetting" + +export async function POST(req) { + const session = await getServerSession(req) + + if (!session) { + return NextResponse.json("Unauthorized") + } else { + try { + const { videoId,videoTitle } = await req.json() + await connectMongoDB() + const videoTitleTrim =videoTitle?.trim() + await salesMeeting.create({ videoId ,videoTitle:videoTitleTrim}) + + return NextResponse.json({ message: "Meeting added." }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while addiing meeting." }, + { status: 500 } + ) + } + } +} + +export async function GET(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + try { + await connectMongoDB() + const meetings = await salesMeeting.find().select() + return NextResponse.json({ meetings }) + } catch (error) { + console.log(error) + return NextResponse.json( + { message: "An error occurred while fetching meetings." }, + { status: 500 } + ) + } + } +} + +export async function DELETE(request) { + const session = await getServerSession(request) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const id = request.nextUrl.searchParams.get("id") + await connectMongoDB() + await salesMeeting.findByIdAndDelete(id) + return NextResponse.json({ message: "Meeting deleted" }, { status: 200 }) + } +} diff --git a/app/api/sendEmail/admin/route.js b/app/api/sendEmail/admin/route.js new file mode 100644 index 0000000..88ec1a4 --- /dev/null +++ b/app/api/sendEmail/admin/route.js @@ -0,0 +1,46 @@ +import { NextResponse } from "next/server" +import { Resend } from "resend" +import { getServerSession } from "next-auth" +import { EmailPlaceholder } from "../../../../components/email-template/email-placeholder" +import { PasswordResetRequest } from "../../../../components/email-template/ResetTemplate" + +const resend = new Resend(process.env.RESEND_API_KEY) + +export async function POST(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + }else{ + try { + const { from, to, subject, projectName, salesPerson } = await req.json() + let data + if(from && to &&!projectName){ + data = await resend.emails.send({ + from: from, + to: to, + subject: "Reset password request", + react: PasswordResetRequest({ + salesName: salesPerson, + mainEmail:from + }), + }) + }else{ + data = await resend.emails.send({ + from: from, + to: to, + subject: subject, + react: EmailPlaceholder({ + projectName: projectName, + salesPerson: salesPerson, + subject: subject, + }), + }) + } + return NextResponse.json(data) + } catch (error) { + console.error("Error:", error) + return NextResponse.json({ error: "An error occurred" }) + } + } + +} diff --git a/app/api/sendEmail/invitation/route.js b/app/api/sendEmail/invitation/route.js new file mode 100644 index 0000000..a504f96 --- /dev/null +++ b/app/api/sendEmail/invitation/route.js @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server" +import { Resend } from "resend" +import { getServerSession } from "next-auth" +import { DefaultTemplate } from "../../../../components/email-template/DefaultTemplate" + +const resend = new Resend(process.env.RESEND_API_KEY) + +export async function POST(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + }else{ + try { + const { from, to, subject, name, upseller } = await req.json() + + let data + + data = await resend.emails.send({ + from: from, + to: to, + subject: subject, + react: DefaultTemplate({ + name: name, + upseller: upseller, + }), + }) + + return NextResponse.json(data) + } catch (error) { + console.error("Error:", error) + return NextResponse.json({ error: "An error occurred" }) + } + } + +} diff --git a/app/api/sendEmail/signRequest/route.js b/app/api/sendEmail/signRequest/route.js new file mode 100644 index 0000000..647dc5c --- /dev/null +++ b/app/api/sendEmail/signRequest/route.js @@ -0,0 +1,38 @@ +import { NextResponse } from "next/server" +import { Resend } from "resend" +import { getServerSession } from "next-auth" +import { RaycastMagicLinkEmail } from "../../../../components/email-template/email-send" + +const resend = new Resend(process.env.RESEND_API_KEY) + +export async function POST(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + }else{ + try { + const { from, to, subject, salesPerson, companyName, rate } = + await req.json() + + let data + + data = await resend.emails.send({ + from: from, + to: to, + subject: subject, + react: RaycastMagicLinkEmail({ + salesPerson: salesPerson, + companyName: companyName, + rate: rate, + subject: subject, + }), + }) + + return NextResponse.json(data) + } catch (error) { + console.error("Error:", error) + return NextResponse.json({ error: "An error occurred" }) + } + } + +} diff --git a/app/api/sendEmail/user/route.js b/app/api/sendEmail/user/route.js new file mode 100644 index 0000000..a9f4223 --- /dev/null +++ b/app/api/sendEmail/user/route.js @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server" +import { Resend } from "resend" + +import { RaycastMagicLinkEmail } from "../../../../components/email-template/email-send" + +const resend = new Resend(process.env.RESEND_API_KEY) + +export async function POST(req) { + try { + const { from, to, subject, projectName, salesPerson, EmailTemplate } = + await req.json() + + let data + + data = await resend.emails.send({ + from: from, + to: to, + subject: subject, + react: RaycastMagicLinkEmail({ + projectName: projectName, + salesPerson: salesPerson, + subject: subject, + }), + }) + + return NextResponse.json(data) // Return the data on success + } catch (error) { + console.error("Error:", error) + return NextResponse.json({ error: "An error occurred" }) + } +} diff --git a/app/api/signin/route.js b/app/api/signin/route.js new file mode 100644 index 0000000..7cea05f --- /dev/null +++ b/app/api/signin/route.js @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server" +import User from "@/models/user" +import bcrypt from "bcryptjs" + +import { connectMongoDB } from "@/lib/mongodb" + +export async function POST(req) { + try { + const { name, email, password } = await req.json() + const hashedPassword = await bcrypt.hash(password, 10) + await connectMongoDB() + const nameTrim=name?.trim() + const emailTrim=email?.trim() + await User.create({ name:nameTrim, email:emailTrim, password: hashedPassword }) + + return NextResponse.json({ message: "User registered." }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while registering the user." }, + { status: 500 } + ) + } +} diff --git a/app/api/stat/route.js b/app/api/stat/route.js new file mode 100644 index 0000000..b0b0311 --- /dev/null +++ b/app/api/stat/route.js @@ -0,0 +1,51 @@ +import { NextResponse } from "next/server" +import Invoice from "@/models/invoice" +import Project from "@/models/project" +import User from "@/models/user" +import { getServerSession } from "next-auth" +import { connectMongoDB } from "@/lib/mongodb" + +export async function GET(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + }else{ + try { + const salesId = req.nextUrl.searchParams.get("id") + await connectMongoDB() + const AllProject = await Project.find({ salesId }) + const projects = AllProject.length + let clientNamesSet = new Set() + AllProject.forEach(function (project) { + clientNamesSet.add(project.clientName) + }) + + + let uniqueClientNames = Array.from(clientNamesSet) + let clients = uniqueClientNames.length + const userId = salesId + const invoices = await Invoice.find({ + userId, + commission_paid: { $ne: "No" }, + }).select() + + let totalAmount = 0 + let commission =0 + + invoices.forEach( (invoice)=> { + const existProject = AllProject?.find((p) => p._id == invoice.projectId); + if (existProject) { + totalAmount+=invoice.amount + const commissionRate = existProject.commisson_rate; + commission += (invoice.amount * invoice.rate) / 100; + } + + }) + + return NextResponse.json({ projects, clients, totalAmount, commission }) + } catch (error) { + console.error(error) // Log errors with console.error for better visibility + return NextResponse.json({ error: "An error occurred" }, { status: 500 }) + } + } +} diff --git a/app/api/upload/route.js b/app/api/upload/route.js new file mode 100644 index 0000000..70d88e8 --- /dev/null +++ b/app/api/upload/route.js @@ -0,0 +1,24 @@ +// pages/api/upload.js +import { getServerSession } from "next-auth" +import { parser } from "../../../lib/cloudinary" + + +export default async function handler(req, res) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + }else{ + try { + if (req.method === "POST") { + await parser.single("image")(req, res) + res.json({ message: "Image uploaded successfully" }) + } else { + res.status(405).json({ error: "Method not allowed" }) + } + } catch (error) { + console.error("Error uploading image", error) + res.status(500).json({ error: "Error uploading image" }) + } + } + +} diff --git a/app/api/user/avatar/route.js b/app/api/user/avatar/route.js new file mode 100644 index 0000000..2047fc5 --- /dev/null +++ b/app/api/user/avatar/route.js @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server" +import User from "@/models/user" +import { getServerSession } from "next-auth" +import { connectMongoDB } from "@/lib/mongodb" + +export async function PUT(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("protected api") + }else{ + try { + const id = req.nextUrl.searchParams.get("id") + const { avatar } = await req.json() + await connectMongoDB() + await User.findByIdAndUpdate(id, { + avatar, + }) + + return NextResponse.json({ message: "Avatar Updated!" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while registering the user." }, + { status: 500 } + ) + } + } + +} diff --git a/app/api/user/route.js b/app/api/user/route.js new file mode 100644 index 0000000..436b062 --- /dev/null +++ b/app/api/user/route.js @@ -0,0 +1,132 @@ +import { NextResponse } from "next/server" +import User from "@/models/user" +import { getServerSession } from "next-auth" + +import { connectMongoDB } from "@/lib/mongodb" + +export async function GET(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + await connectMongoDB() + const _id = req.nextUrl.searchParams.get("id") + const role = req.nextUrl.searchParams.get("role") + const name = req.nextUrl.searchParams.get("name") + const upSellerId = req.nextUrl.searchParams.get("upSellerId") + const admin = req.nextUrl.searchParams.get("admin") + + if (_id && !name && !role && !upSellerId && !admin) { + try { + await connectMongoDB() + const user = await User.findOne({ _id: _id }) + return NextResponse.json({ user }) + } catch (error) { + console.log(error) + } + } + + if (!_id && name && !role && !upSellerId && !admin) { + try { + await connectMongoDB() + const users = await User.findOne({ name }) + return NextResponse.json({ users }) + } catch (error) { + console.log(error) + } + } + + if (!_id && !name && role && !upSellerId && !admin) { + try { + await connectMongoDB() + const users = await User.find({ role }) + return NextResponse.json({ users }) + } catch (error) { + console.log(error) + } + } + + if (!_id && !name && !role && upSellerId && !admin) { + try { + await connectMongoDB() + const users = await User.find({ upSellerId }) + return NextResponse.json({ users }) + } catch (error) { + console.log(error) + } + } + if (!_id && !name && !role && !upSellerId && !admin) { + try { + await connectMongoDB() + const users = await User.find({ role: { $in: ["Sales1", "Sales2"] } }) + return NextResponse.json({ users }) + } catch (error) { + console.log(error) + } + } + if (!_id && !name && !role && !upSellerId && admin) { + if (admin == "SuperAdmin") { + try { + await connectMongoDB() + const users = await User.find({ role: { $ne: admin } }) + return NextResponse.json({ users }) + } catch (error) { + console.log(error) + } + } else { + try { + await connectMongoDB() + const users = await User.find({ + role: { $ne: ["SuperAdmin", admin] }, + }) + return NextResponse.json({ users }) + } catch (error) { + console.log(error) + } + } + } + } +} + +export async function PUT(req) { + const session = await getServerSession(req) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + try { + const id = req.nextUrl.searchParams.get("id") + const { name, role, commission_rate, upSeller, upSellerId, contracts } = + await req.json() + await connectMongoDB() + const nameTrim = name?.trim() + const roleTrim = role?.trim() + await User.findByIdAndUpdate(id, { + name:nameTrim, + role:roleTrim, + upsellerPercentage: commission_rate, + upSeller, + upSellerId, + contracts, + }) + + return NextResponse.json({ message: "User Updated!" }, { status: 201 }) + } catch (error) { + return NextResponse.json( + { message: "An error occurred while registering the user." }, + { status: 500 } + ) + } + } +} + +export async function DELETE(request) { + const session = await getServerSession(request) + if (!session) { + return NextResponse.json("Unauthorized") + } else { + const id = request.nextUrl.searchParams.get("id") + await connectMongoDB() + await User.findByIdAndDelete(id) + return NextResponse.json({ message: "User deleted" }, { status: 200 }) + } +} diff --git a/app/api/userExists/route.js b/app/api/userExists/route.js new file mode 100644 index 0000000..8b0c3e3 --- /dev/null +++ b/app/api/userExists/route.js @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from "next/server" +import User from "@/models/user" +import { getServerSession } from "next-auth" +import { connectMongoDB } from "@/lib/mongodb" + + +export async function POST(req) { + try { + await connectMongoDB() + const { email } = await req.json() + const user = await User.findOne({ email }).select("_id") + return NextResponse.json({ user }) + } catch (error) { + console.log(error) + } + +} + + +export async function GET(req) { + try { + const email = req.nextUrl.searchParams.get("email"); + await connectMongoDB(); + const user = await User.findOne({ email }).select() + return NextResponse.json({ user }); + } catch (error) { + console.log(error) + } + +} \ No newline at end of file diff --git a/app/company/[name]/page.tsx b/app/company/[name]/page.tsx new file mode 100644 index 0000000..0692a47 --- /dev/null +++ b/app/company/[name]/page.tsx @@ -0,0 +1,10 @@ +import Company from "../../../components/company/Company" + +export default async function SingleCompany({ params }) { + const companyName = params?.name.toLowerCase() + return ( +
+ +
+ ) +} diff --git a/app/contracts/ContactsSkelaton.tsx b/app/contracts/ContactsSkelaton.tsx new file mode 100644 index 0000000..16666c4 --- /dev/null +++ b/app/contracts/ContactsSkelaton.tsx @@ -0,0 +1,14 @@ +import React from "react" + +import { Skeleton } from "@/components/ui/skeleton" + +const ContactsSkeleton = () => { + return ( +
+ + +
+ ) +} + +export default ContactsSkeleton diff --git a/app/contracts/page.tsx b/app/contracts/page.tsx new file mode 100644 index 0000000..7ac23e9 --- /dev/null +++ b/app/contracts/page.tsx @@ -0,0 +1,197 @@ +"use client" + +import React, { useEffect, useState } from "react" +import Image from "next/image" +import Link from "next/link" +import { CheckCircle2, CheckSquare } from "lucide-react" +import { useSession } from "next-auth/react" + +import { fetchCompanies } from "@/lib/company/company" +import { getUser } from "@/lib/getUser" +import { Button } from "@/components/ui/button" +import SignContract from "@/components/company/SignContract" + +import ContactsSkeleton from "./ContactsSkelaton" + +export const dynamic = 'force-dynamic'; + +function replaceSpaceAndLowerCase(inputString) { + const result = inputString.replace(" ", "-").toLowerCase() + return result +} + +export default function ContractsPage() { + const { data: session } = useSession() + const role = session?.user.role + const [currentUsername, setcurrentUsername] = useState() + const [currentEmail, setcurrentEmail] = useState() + const [activeContracts, setactiveContracts] = useState([]) + const [allCompany, setallCompanies] = useState([]) + + const userDetail = (id) => { + getUser(id).then((res: Array) => { + setcurrentEmail(res.email) + console.log(res) + if (role == "Admin-IA") { + setactiveContracts( + res?.contracts?.filter((item) => item.companyName == "image appeal") + ) + setcurrentUsername(res?.name) + } else { + setactiveContracts(res.contracts) + setcurrentUsername(res.name) + } + }) + } + + const allCompanies = () => { + fetchCompanies().then((all) => { + console.log(all) + if (role == "Admin-IA") { + setallCompanies([]) + } else { + setallCompanies(all) + } + }) + } + + useEffect(() => { + if (session) { + const id = session?.user?.id + + userDetail(id) + allCompanies() + } + }, [session]) + + return ( + <> +
+

Active Contracts

+
+ {!activeContracts && ( +
+ + + + + +
+ )} + {activeContracts && + activeContracts.map((company, index) => ( + +
+
+ company logo +
+

{company.companyType}

+
+
+ + Commission: {company.rate}% +
+
+
+ + ))} +
+ + <> + {allCompany?.length > 0 && ( +

Other Offers

+ )} +
+ {!allCompany && ( +
+ + + + + + +
+ )} + {allCompany && + allCompany + .filter((company) => + activeContracts?.every( + (item) => item.companyName !== company.companyName + ) + ) + .map((scompany, index) => { + if ( + scompany.companyName !== "WordSphere" && + scompany.companyName !== "image appeal" + ) { + return ( +
+
+ + company logo + +
+

+ {scompany.companyType} +

+
+ +
+
+ + + Commission: {scompany.rate}% + +
+
+ ) + } else { + return null // Skip rendering for "wordsphere" + } + })} +
+ +
+ + ) +} diff --git a/app/dashboard/AdminIA.tsx b/app/dashboard/AdminIA.tsx new file mode 100644 index 0000000..b42702b --- /dev/null +++ b/app/dashboard/AdminIA.tsx @@ -0,0 +1,169 @@ +"user client" + +import React, { useEffect, useState } from "react" +import { useSession } from "next-auth/react" + +import { fetchCompanies } from "@/lib/company/company" +import { + fetchInvoiceEarningByCompanyName, + fetchInvoices, +} from "@/lib/fetchInvoices" +import { fetchProjectById, fetchProjects } from "@/lib/fetchProjects" +import { fetchAllUsers, fetchUsers } from "@/lib/fetchUsers" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import SellerList from "@/components/SellerList" +import StatCard from "@/components/StatCard" +import AlertBox from "@/components/common-ui/AlertBox" +import Datatable from "@/components/datatable" +import CardSkeleton from "@/components/skeleton/CardSkeleton" +import MainSkeleton from "@/components/skeleton/MainSkeleton" + +export default function AdminIA(props) { + const { data: session } = useSession() + const [data, setData] = useState(null) + const [clientName, setclientName] = useState() + const [salesPerson, setSalesPerson] = useState([]) + const [pendingProject, setpendingProject] = useState() + const [completedproject, setcompletedproject] = useState() + const [isLoading, setLoading] = useState(true) + + const [totalEarn, settotalEarn] = useState(0) + const [upSellerCommission, setUpSellerCommission] = useState(0) + const role = session?.user?.role + const username = session?.user?.name + const userId = session?.user?.id + const [userData, setuserData] = useState([]) + const [Allcompanies, setAllcompanies] = useState() + + useEffect(() => { + fetchProjects() + .then((apiData) => { + apiData = apiData.filter((item) => item.companyName == "image appeal") + + if (apiData) { + const pendingProjects = apiData.filter( + (item) => item.status == "Pending" + ) + + const completedproject = apiData.filter( + (item) => item.status == "Complete" + ) + + const uniqueClientNames = [] + + apiData.forEach((item) => { + if (!uniqueClientNames.includes(item.clientName)) { + uniqueClientNames.push(item.clientName) + } + }) + + const uniqueSales = [] + apiData.forEach((item) => { + if (!uniqueSales.includes(item.salesPerson)) { + uniqueSales.push(item.salesPerson) + } + }) + + setcompletedproject(completedproject) + // setSalesPerson(uniqueSales) + setclientName(uniqueClientNames) + setpendingProject(pendingProjects) + setData(apiData) + setLoading(false) + } + }) + .catch((error) => { + console.error("Error in component:", error) + }) + + fetchInvoiceEarningByCompanyName("image appeal").then((res) => { + settotalEarn(res) + }) + fetchUsers("Sales1") + .then((userData) => {}) + .catch((error) => { + console.error("Error in component:", error) + }) + + fetchUsers("Sales2") + .then((salesGuy) => { + setSalesPerson(salesGuy) + }) + .catch((error) => { + console.error("Error in component:", error) + }) + + fetchCompanies().then((companies) => { + const demo = companies.filter( + (item) => item.companyName == "image appeal" + ) + setAllcompanies(demo) + }) + setLoading(false) + }, []) + + useEffect(() => { + fetchAllUsers().then((user) => { + setuserData(user) + }) + }, []) + + if (!data) { + return + } + + return ( + <> +
+ {pendingProject && pendingProject.length > 0 && ( + + )} +
+ {!salesPerson && } + {salesPerson && ( + + )} + {!data && } + {data && } + + {!clientName && } + {clientName && ( + + )} + + {!totalEarn && totalEarn != 0 && } + {(totalEarn || totalEarn == 0) && ( + + )} +
+ +
+ + + Projects + + + Sales Rep + + +
+ + {Allcompanies && data && ( + + )} + + + {userData && } + +
+
+ + ) +} diff --git a/app/dashboard/DashboardClient.tsx b/app/dashboard/DashboardClient.tsx new file mode 100644 index 0000000..fadee2a --- /dev/null +++ b/app/dashboard/DashboardClient.tsx @@ -0,0 +1,97 @@ +"use client" +import { useEffect, useState } from "react" +import { useSession } from "next-auth/react" +import { fetchInvoices } from "@/lib/fetchInvoices" +import { fetchProjects } from "@/lib/fetchProjects" + +import AdminIA from "./AdminIA" +import SalesOne from "./SalesOne" +import SalesTwo from "./SalesTwo" +import SuperAdmin from "./SuperAdmin" + + +export default function DashboardPage() { + const { data: session } = useSession() + const role = session?.user?.role + const username = session?.user?.name + + // TODO: The data is now set correctly to avoid runtime errors if hit, but the use of the data itself may be plausible to review + const [completedProject, setCompletedProject] = useState() + const [pendingProject, setPendingProject] = useState() + const [clientName, setClientName] = useState() + const [data, setData] = useState() + const [loading, setLoading] = useState(true) + const [totalEarn, setTotalEarn] = useState() + + useEffect(() => { + if (session && role == "Sales2") { + fetchProjects() + .then((apiData) => { + if (apiData) { + const pendingProjects = apiData.filter( + (item) => item.status == "Pending" && item.salesPerson == username + ) + const filteredData = apiData.filter( + (item) => + item.status !== "Pending" && item.salesPerson == username + ) + + const completedproject = apiData.filter( + (item) => + item.status == "Complete" && item.salesPerson == username + ) + + const uniqueClientNames = [] + + filteredData.forEach((item) => { + if (!uniqueClientNames.includes(item.clientName)) { + uniqueClientNames.push(item.clientName) + } + }) + + setCompletedProject(completedproject) + setClientName(uniqueClientNames) + setPendingProject(pendingProjects) + setData(filteredData) + setLoading(false) + } + }) + .catch((error) => { + console.error("Error in component:", error) + }) + + fetchInvoices() + .then((invoiceData) => { + if (invoiceData) { + const earning = invoiceData.filter( + (item) => + item.status == "Paid" && + item.commission_paid == "Yes" && + item.userId == session?.user?.id + ) + + const total = earning.reduce( + (total, item) => total + (item.amount / 100) * item.rate, + 0 + ) + + setTotalEarn(total) + } + }) + .catch((error) => { + console.error("Error in component:", error) + }) + } + }, [session, role, username]) + + return ( + <> +
+ {role == "SuperAdmin" && } + {role == "Admin-IA" && } + {role == "Sales1" && } + {role == "Sales2" && } +
+ + ) +} diff --git a/app/dashboard/SalesOne.tsx b/app/dashboard/SalesOne.tsx new file mode 100644 index 0000000..c10cc5d --- /dev/null +++ b/app/dashboard/SalesOne.tsx @@ -0,0 +1,167 @@ +"use client" + +import { useEffect, useState } from "react" +import { useSession } from "next-auth/react" + +import { fetchCompanies } from "@/lib/company/company" +import { fetchProjects } from "@/lib/fetchProjects" +import { fetchUsers } from "@/lib/fetchUsers" +import { InvoiceByUserId,upSellerPercentage } from "@/lib/fetchInvoices" +import { getUser } from "@/lib/getUser" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import SalesTable from "@/components/SalesTable" +import SingleCard from "@/components/SingleCard" +import StatCard from "@/components/StatCard" +import AlertBox from "@/components/common-ui/AlertBox" +import DatatableSeller1 from "@/components/datatableSeller1" +import CardSkeleton from "@/components/skeleton/CardSkeleton" + +import TableRowSkeleton from "./TableRowSkeleton" + +export default function SalesOne() { + const { data: session } = useSession() + const [data, setData] = useState(null) + const [clientName, setclientName] = useState() + const [pendingProject, setpendingProject] = useState() + + const role = session?.user?.role + const username = session?.user?.name + const [userData, setuserData] = useState() + + const [salesGuy, setsalesGuy] = useState() + const [earning, setEarning] = useState(0) + const [commission, setcommission] = useState(0) + + const [Allcompanies, setAllcompanies] = useState() + + const [userId, setuserId] = useState() + const [userName, setuserName] = useState() + + const [Downstream, setDownstream] = useState() + + useEffect(() => { + if (session) { + const id = session?.user?.id + + fetch(`/api/user?upSellerId=${id}`) + .then((res) => res.json()) + .then((data) => { + setsalesGuy(data.users.length) + }) + .catch((error) => { + console.error("Error:", error) + }) + + InvoiceByUserId(session.user.id).then((res)=>{ + setEarning(res) + }) + upSellerPercentage(session.user.id).then((res)=>{ + setcommission(res) + }) + + fetchUsers("Sales2") + .then((userData) => { + const Downstream = userData.filter((item) => item.upSellerId == id) + setDownstream(Downstream) + }) + .catch((error) => { + console.error("Error in component:", error) + }) + + fetchProjects() + .then((apiData) => { + if (apiData) { + const allProjects = apiData.filter((item) => item.salesId == id) + const uniqueClientNames = [] + + allProjects.forEach((item) => { + if (!uniqueClientNames.includes(item.clientName)) { + uniqueClientNames.push(item.clientName) + } + }) + + setclientName(uniqueClientNames.length) + + setData(allProjects) + + const pendingProjects = allProjects.filter( + (item) => item.status == "Pending" + ) + setpendingProject(pendingProjects) + } + }) + .catch((error) => { + console.error("Error in component:", error) + }) + + setuserId(session?.user?.id) + setuserName(session?.user?.name) + + if (session) { + const id = session?.user?.id + getUser(id).then((user) => { + setAllcompanies(user.contracts) + }) + } + } + }, [session]) + + return ( + <> + {pendingProject && pendingProject.length > 0 && ( + + )} +
+ {!session && } + {session && } + + {!clientName && clientName != 0 && } + + + {!session && } + {(earning || earning == 0) && ( + <> + + + )} + {(commission || commission == 0) && ( + <> + + + )} +
+
+ +
+ + + Projects + + + Downstream Reps + + +
+ + {!data && } + + {userId && userName && data && Allcompanies && ( + + )} + + + {Downstream && } + +
+
+ + ) +} diff --git a/app/dashboard/SalesTwo.tsx b/app/dashboard/SalesTwo.tsx new file mode 100644 index 0000000..406a4de --- /dev/null +++ b/app/dashboard/SalesTwo.tsx @@ -0,0 +1,119 @@ +"use client" + +import { useEffect, useState } from "react" +import { useSession } from "next-auth/react" + +import { fetchCompanies } from "@/lib/company/company" +import { fetchInvoices,InvoiceByUserId } from "@/lib/fetchInvoices" +import { fetchProjects } from "@/lib/fetchProjects" +import { getUser } from "@/lib/getUser" +import StatCard from "@/components/StatCard" +import AlertBox from "@/components/common-ui/AlertBox" +import DatatableSales from "@/components/datatableSales" +import CardSkeleton from "@/components/skeleton/CardSkeleton" + +import TableRowSkeleton from "./TableRowSkeleton" + +export default function SalesTwo() { + const { data: session } = useSession() + const [data, setData] = useState(null) + const [clientName, setclientName] = useState() + const [salesPerson, setSalesPerson] = useState() + const [pendingProject, setpendingProject] = useState() + const [completedproject, setcompletedproject] = useState() + const [isLoading, setLoading] = useState(true) + + const [totalEarn, settotalEarn] = useState(0) + const role = session?.user?.role + const username = session?.user?.name + + const [AllCompanies, setAllcompanies] = useState() + const [allinvoice, setallInvoice] = useState() + + useEffect(() => { + fetchProjects() + .then((apiData) => { + if (apiData) { + const pendingProjects = apiData.filter( + (item) => item.status == "Pending" && item.salesPerson == username + ) + const filteredData = apiData.filter( + (item) => item.status !== "Pending" && item.salesPerson == username + ) + + const completedproject = apiData.filter( + (item) => item.status == "Complete" && item.salesPerson == username + ) + + const uniqueClientNames = [] + + filteredData.forEach((item) => { + if (!uniqueClientNames.includes(item.clientName)) { + uniqueClientNames.push(item.clientName) + } + }) + + setcompletedproject(completedproject) + setclientName(uniqueClientNames) + setpendingProject(pendingProjects) + setData(filteredData) + setLoading(false) + } + }) + .catch((error) => { + console.error("Error in component:", error) + }) + + InvoiceByUserId(session?.user?.id).then((res)=>{ + settotalEarn(res) + }) + if (session) { + const id = session?.user?.id + getUser(id).then((user) => { + console.log(user) + setAllcompanies(user.contracts) + }) + } + }, [session, role, username]) + + return ( + <> + {pendingProject && pendingProject.length > 0 && ( + + )} + +
+ {!totalEarn && totalEarn != 0 && } + {(totalEarn || totalEarn == 0) && ( + + )} + + {!data && } + {data && ( + + )} + + {!clientName && } + {clientName && ( + + )} +
+ + {!data && } + + {data && AllCompanies && ( + + )} + + ) +} diff --git a/app/dashboard/SuperAdmin.tsx b/app/dashboard/SuperAdmin.tsx new file mode 100644 index 0000000..c634341 --- /dev/null +++ b/app/dashboard/SuperAdmin.tsx @@ -0,0 +1,164 @@ +"user client" + +import React, { useEffect, useState } from "react" +import { useSession } from "next-auth/react" + +import { fetchCompanies } from "@/lib/company/company" +import { fetchInvoiceEarning, fetchInvoices } from "@/lib/fetchInvoices" +import { fetchProjectById, fetchProjects } from "@/lib/fetchProjects" +import { fetchAllUsers, fetchUsers } from "@/lib/fetchUsers" +import { Skeleton } from "@/components/ui/skeleton" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import SellerList from "@/components/SellerList" +import StatCard from "@/components/StatCard" +import AlertBox from "@/components/common-ui/AlertBox" +import Datatable from "@/components/datatable" +import CardSkeleton from "@/components/skeleton/CardSkeleton" +import MainSkeleton from "@/components/skeleton/MainSkeleton" + +export default function SuperAdmin(props) { + const { data: session } = useSession() + const [data, setData] = useState(null) + const [clientName, setclientName] = useState() + const [salesPerson, setSalesPerson] = useState([]) + const [pendingProject, setpendingProject] = useState() + const [completedproject, setcompletedproject] = useState() + const [isLoading, setLoading] = useState(true) + + const [totalEarn, settotalEarn] = useState(0) + const [upSellerCommission, setUpSellerCommission] = useState(0) + const role = session?.user?.role + const username = session?.user?.name + const userId = session?.user?.id + const [userData, setuserData] = useState([]) + const [Allcompanies, setAllcompanies] = useState() + + useEffect(() => { + fetchProjects() + .then((apiData) => { + if (apiData) { + const pendingProjects = apiData.filter( + (item) => item.status == "Pending" + ) + const completedproject = apiData.filter( + (item) => item.status == "Complete" + ) + const uniqueClientNames = [] + + apiData.forEach((item) => { + if (!uniqueClientNames.includes(item.clientName)) { + uniqueClientNames.push(item.clientName) + } + }) + + const uniqueSales = [] + apiData.forEach((item) => { + if (!uniqueSales.includes(item.salesPerson)) { + uniqueSales.push(item.salesPerson) + } + }) + + setcompletedproject(completedproject) + // setSalesPerson(uniqueSales) + setclientName(uniqueClientNames) + setpendingProject(pendingProjects) + setData(apiData) + setLoading(false) + } + }) + .catch((error) => { + console.error("Error in component:", error) + }) + + fetchInvoiceEarning().then((res) => { + settotalEarn(res) + }) + fetchUsers("Sales1") + .then((userData) => {}) + .catch((error) => { + console.error("Error in component:", error) + }) + fetchUsers("Sales2") + .then((salesGuy) => { + setSalesPerson(salesGuy) + }) + + .catch((error) => { + console.error("Error in component:", error) + }) + + fetchCompanies().then((companies) => { + setAllcompanies(companies) + }) + setLoading(false) + }, []) + + useEffect(() => { + fetchAllUsers().then((users) => { + setuserData(users) + }) + }, []) + + if (!data) { + return ( +
+ +
+ ) + } + + return ( + <> +
+ {pendingProject && pendingProject.length > 0 && ( + + )} +
+ {!salesPerson && } + {salesPerson && ( + + )} + {!data && } + {data && } + + {!clientName && } + {clientName && ( + + )} + + {!totalEarn && totalEarn != 0 && } + {(totalEarn || totalEarn == 0) && ( + + )} +
+ +
+ + + Projects + + + Sales Rep + + +
+ + {Allcompanies && data && ( + + )} + + + {userData && } + +
+
+ + ) +} diff --git a/app/dashboard/TableRowSkeleton.tsx b/app/dashboard/TableRowSkeleton.tsx new file mode 100644 index 0000000..888a629 --- /dev/null +++ b/app/dashboard/TableRowSkeleton.tsx @@ -0,0 +1,105 @@ +import React from "react" + +import { Skeleton } from "@/components/ui/skeleton" + +const TableRowSkeleton = () => { + return ( +
+
+
+ + + +
+
+ +
+
+ +
+
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ ) +} + +export default TableRowSkeleton diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..5c43473 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,8 @@ +// app/dashboard/page.tsx +import DashboardClient from "./DashboardClient" + +export const dynamic = 'force-dynamic'; + +export default function DashboardPage() { + return +} \ No newline at end of file diff --git a/app/edit/page.tsx b/app/edit/page.tsx new file mode 100644 index 0000000..b3224d5 --- /dev/null +++ b/app/edit/page.tsx @@ -0,0 +1,46 @@ +"use client" + +import React, { useEffect, useState } from "react" + +export default function ProjectDetails({ params }) { + const slug = params.slug + const [data, setData] = useState(null) + const [isLoading, setLoading] = useState(true) + + useEffect(() => { + fetch(`/api/project/?slug=${slug}`) + .then((res) => res.json()) + .then((data) => { + const project = data.project + setData(project) + setLoading(false) + }) + // eslint-disable-next-line + }, []) + + if (isLoading) { + return ( +
+

loading...

+
+ ) + } + + if (!data) return

No project data

+ return ( + <> +
+

{data.projectName}

+

{data.projectDetails}

+

{data.budget}

+

{data.commisson_rate}

+

{data.dateSigned}

+

{data.status}

+

{data.clientName}

+

{data.email}

+

{data.phone}

+

{data.address}

+
+ + ) +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..6a75725 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/app/invitation/page.tsx b/app/invitation/page.tsx new file mode 100644 index 0000000..f5f36d3 --- /dev/null +++ b/app/invitation/page.tsx @@ -0,0 +1,54 @@ +"use client" + +import Image from "next/image" +import Link from "next/link" +import { redirect } from "next/navigation" +import { getServerSession } from "next-auth" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" +import SetPassword from "@/components/SetPassword" + +import { authOptions } from "./../api/auth/[...nextauth]/route" + +export const dynamic = "force-dynamic" + +export default async function InvitationPage() { + const session = await getServerSession(authOptions) + if (session) redirect("/dashboard") + + return ( + <> +
+ + Login + +
+
+
+ + Logo + +
+
+
+
+
+

+ Welcome to SalesFam +

+

Set your password

+
+ +
+
+
+ + ) +} diff --git a/app/landing/page.tsx b/app/landing/page.tsx new file mode 100644 index 0000000..579242c --- /dev/null +++ b/app/landing/page.tsx @@ -0,0 +1,25 @@ +import Clients from "@/components/landing/Clients" +import Help from "@/components/landing/Help" +import Hero from "@/components/landing/Hero" +import Pricing from "@/components/landing/Pricing" +import ProfileSlider from "@/components/landing/ProfileSlider" +import Software from "@/components/landing/Software" +import Success from "@/components/landing/Success" +import Footer from "@/components/landing/common/Footer" +import Header from "@/components/landing/common/Header" + +export default function Home() { + return ( + <> +
+ + + + + + + {/* */} +
+ + ) +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..4f7e6b6 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,58 @@ +import { Work_Sans } from "next/font/google" + +import "@/styles/globals.css" +import { Metadata } from "next" + +import { siteConfig } from "@/config/site" +import { cn } from "@/lib/utils" +import { Toaster } from "@/components/ui/toaster" +import Header from "@/components/Header" +import HeaderComp from "@/components/common-ui/HeaderComp" + +import { AuthProvider } from "./Providers" + +const worksans = Work_Sans({ + weight: ["400", "500"], + subsets: ["latin"], + variable:['--work-sans'] +}) + +export const metadata: Metadata = { + title: { + default: siteConfig.name, + template: `%s - ${siteConfig.name}`, + }, + description: siteConfig.description, + metadataBase: new URL("https://salesfam.com"), + openGraph: { + title: { + default: siteConfig.name, + template: `%s - ${siteConfig.name}`, + }, + description: siteConfig.description, + images: [{ url: "/salesfam-og.jpg", alt: "Sales Fam" }], + }, + icons: { + icon: "/favicon.ico", + shortcut: "/favicon.ico", + apple: "/favicon.ico", + }, +} + +export default function RootLayout({ children }) { + return ( + <> + + + +
+ +
{children}
+ +
+
+ + + + ) +} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..3c064aa --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,52 @@ +import Image from "next/image" +import Link from "next/link" +import { redirect } from "next/navigation" +import { getServerSession } from "next-auth" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" +import { Loginform } from "@/components/Loginform" + +import { authOptions } from "./../api/auth/[...nextauth]/route" + +export default async function Login() { + const session = await getServerSession(authOptions) + + if (session) redirect("/dashboard") + return ( + <> +
+ + Signup + +
+
+
+ + Logo + +
+
+
+
+
+

+ Login to your Account +

+

+ Enter your email and password below to login +

+
+ +
+
+
+ + ) +} diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..1338ea6 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,20 @@ +import React from "react" +import Link from "next/link" + +import { Button } from "@/components/ui/button" + +export default function notFound() { + return ( +
+
+

404!

+

+ Oops! This Page Could Not Be Found. +

+ +
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..4adccc0 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,33 @@ +import { Suspense } from "react" + +import ReferralComp from "@/components/ReferralComp" +import Clients from "@/components/landing/Clients" +import Help from "@/components/landing/Help" +import Hero from "@/components/landing/Hero" +import Pricing from "@/components/landing/Pricing" +import ProfileSlider from "@/components/landing/ProfileSlider" +import Software from "@/components/landing/Software" +import Success from "@/components/landing/Success" +import Footer from "@/components/landing/common/Footer" +import Header from "@/components/landing/common/Header" + +export default function IndexPage() { + return ( + <> + Loading...
}> + + +
+
+
+ + + + + + + +
+ + ) +} diff --git a/app/project/[slug]/page.tsx b/app/project/[slug]/page.tsx new file mode 100644 index 0000000..ae486cd --- /dev/null +++ b/app/project/[slug]/page.tsx @@ -0,0 +1,503 @@ +"use client" + +import React, { useEffect, useState } from "react" +import { useSearchParams } from "next/navigation" +import Project from "@/models/project" +import { + ArrowUpDown, + BookOpenCheck, + Calendar as CalendarIcon, + ChevronDown, + ExternalLink, + FolderEdit, + MoreVertical, + Plus, + Trash2, +} from "lucide-react" +import { signOut, useSession } from "next-auth/react" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Skeleton } from "@/components/ui/skeleton" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import AddInvoice from "@/components/AddInvoice" +import EditProject from "@/components/EditProject" +import Gettotal from "@/components/Gettotal" +import Gettotalcommission from "@/components/Gettotalcommission" +import InvoiceTable from "@/components/InvoiceTable" +import SalesInvoiceTable from "@/components/SalesInvoiceTable" +import Statusbadge from "@/components/statusBadge" + +export default function ProjectDetails({ params }) { + const slug = params.slug + const { data: session } = useSession() + const [data, setData] = useState(null) + const [commission, setCommission] = useState() + const [upsellerId, setupsellerId] = useState("none") + + const [isLoading, setLoading] = useState(true) + const [isEdit, setEdit] = useState(false) + + const handleToggleEdit = () => { + setEdit((prevEdit) => !prevEdit) + } + + const [invoiceData, setinvoicedata] = useState() + + const findObjectByCompanyName = (array, companyName) => { + return array.find((obj) => obj.companyName === companyName) + } + + useEffect(() => { + fetch(`/api/project/?slug=${slug}`) + .then((res) => res.json()) + .then((data) => { + setData(data.project) + setLoading(false) + + fetch(`/api/user/?id=${data.project.salesId}`) + .then((response) => response.json()) + .then((Userdata) => { + const foundObject = findObjectByCompanyName( + Userdata.user.contracts, + data.project.companyName + ) + setCommission(foundObject.rate) + setupsellerId(Userdata.user.upSellerId) + }) + .catch((error) => { + console.error("Error:", error) + }) + return fetch(`../api/invoice/?projectId=${data.project._id}`) + }) + .then((response) => response.json()) + .then((invoices) => { + setinvoicedata(invoices) + }) + .catch((error) => { + console.error("Error:", error) + }) + }, []) + + if (isLoading) { + return ( +
+
+
+ + + + + + +
+
+ + + +
+
+ + + + +
+
+ + + + +
+
+
+
+
+
+ ) + } + if (!data) return

No project data

+ return ( + <> +
+ + +
+ + + Overview + + + + Invoices + +
+
+ + + {isEdit ? ( + <> + +
+
+
    +
  • + +
  • +
  • + + + + + + + + Are you absolutely sure? + + + This action cannot be undone. This will + permanently delete the project and remove + data from our servers. + + + + Cancel + { + const res = await fetch( + `/api/project?id=${data._id}`, + { + method: "DELETE", + } + ) + if (res.ok) { + window.location.href = "/dashboard" + } + }} + > + Remove + + + + +
  • +
+
+
+
+ + + + + ) : ( + <> + +
+ + {data.projectName} + +
+ {(session?.user?.role == "SuperAdmin" || + session?.user?.role == "Sales1" ||session?.user?.role == "Admin-IA") && ( +
    +
  • + +
  • +
  • + + + + + + + + Are you absolutely sure? + + + This action cannot be undone. This will + permanently delete the project and remove + data from our servers. + + + + + Cancel + + { + const res = await fetch( + `/api/project?id=${data._id}`, + { + method: "DELETE", + } + ) + if (res.ok) { + window.location.href = "/" + } + }} + > + Remove + + + + +
  • +
+ )} +
+
+
+ +
+
+
+
+
+

+ + Sales Person:{" "} + +

+

+ + Company Name:{" "} + +

+

+ + Status:{" "} + +

+

+ + Date Signed:{" "} + +

+

+ + Budget:{" "} + +

+

+ + Commission Rate:{" "} + +

+

+ + Commission Paid:{" "} + +

+
+
+

{data.salesPerson}

+

{data.companyName}

+

+ +

+

{data.dateSigned}

+

${data.budget}

+

{commission && commission}%

+

+ {invoiceData && ( + + )} +

+
+
+
+
+
+
+
+

+ + Client Name:{" "} + +

+

+ + Email Address:{" "} + +

+

+ + Phone Number:{" "} + +

+

+ + Client Address:{" "} + +

+
+
+

{data.clientName}

+

{data.email}

+

{data.phone}

+ +

+ +
+
+
+ {data?.callClient && +
+

Do not call the client directly.

+
} + {data?.emailClient && +
+

Do not send marketing emails to the client.

+
+ } +
+
+
+
+
+

+ + Project Details:{" "} + +

+ +

+
+ + +
+
+ + )} +
+
+ + + + + Invoices + + + + {(session?.user?.role == "SuperAdmin" ||session?.user?.role == "Admin-IA") && ( + + )} +
+ {!invoiceData && <>Loading...} + {(session?.user?.role == "SuperAdmin" ||session?.user?.role == "Admin-IA") && invoiceData && ( + + )} + {session?.user?.role == "Sales1" && invoiceData && ( + + )} + {session?.user?.role == "Sales2" && invoiceData && ( + + )} +
+
+
+
+

+ Payment Due: +

+ + {invoiceData && ( + + )} + +
+
+
+
+

+ Payment Recived: +

+ + {invoiceData && ( + + )} + +
+
+
+
+

+ Commission Paid: +

+ + {invoiceData && ( + + )} + +
+
+
+
+
+
+
+
+ + ) +} diff --git a/app/reset-password/page.tsx b/app/reset-password/page.tsx new file mode 100644 index 0000000..52cacbc --- /dev/null +++ b/app/reset-password/page.tsx @@ -0,0 +1,15 @@ +import React from "react" + +import ResetPassword from "../../components/ResetPassword" + +const page = () => { + return ( +
+
+ +
+
+ ) +} + +export default page diff --git a/app/sales-meeting/page.tsx b/app/sales-meeting/page.tsx new file mode 100644 index 0000000..0a89672 --- /dev/null +++ b/app/sales-meeting/page.tsx @@ -0,0 +1,102 @@ +"use client" + +import React, { useEffect, useState } from "react" +import { Search } from "lucide-react" + +import { DeleteMeetings, fetchMeetings } from "@/lib/salesMetting/salesMetting" +import { ScrollArea } from "@/components/ui/scroll-area" + +import SalesMeeting from "../../components/meetings/SalesMeeting" + +export default function SalesMetting() { + const [meetingList, setMeetingList] = useState([]) + const [isLoading, setLoading] = useState(false) + const [play, setPlay] = useState() + const [searchQuery, setSearchQuery] = useState("") + const [videoId, setVideoId] = useState("") + + useEffect(() => { + setLoading(true) + fetchMeetings().then((data) => { + setLoading(false) + setMeetingList(data) + }) + }, []) + + const handleSearchChange = (e) => { + setSearchQuery(e.target.value) + } + + const filteredMeetingList = meetingList.filter((item) => + item.videoTitle?.toLowerCase().includes(searchQuery?.toLowerCase()) + ) + + return ( +
+

Sales Meetings

+
+
+ +
+
+
+
+ + + + +
+
+ + {isLoading && } + {filteredMeetingList.length > 0 + ? filteredMeetingList.map((item, index) => ( +
{ + setPlay(index), setVideoId(item.videoId) + }} + className={`flex items-center p-4 rounded-md cursor-pointer border-y ${ + play === index && "bg-primary" + }`} + > +
+ +
+
+

+ {item.videoTitle ? item.videoTitle : "No title"} +

+
+
+ )) + : !isLoading && ( +

No video found

+ )} +
+ Total: {filteredMeetingList.length} videos +
+
+
+ ) +} diff --git a/app/sales/[name]/page.tsx b/app/sales/[name]/page.tsx new file mode 100644 index 0000000..30c875e --- /dev/null +++ b/app/sales/[name]/page.tsx @@ -0,0 +1,229 @@ +"use client" + +import React, { useEffect, useState } from "react" +import Image from "next/image" +import { Crown, Trophy } from "lucide-react" + +import { fetchProjects } from "@/lib/fetchProjects" +import { fetchSales } from "@/lib/fetchUsers" +import { Badge } from "@/components/ui/badge" +import { Separator } from "@/components/ui/separator" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import SalesTable from "@/components/SalesTable" +import SellerList from "@/components/SellerList" +import Datatable from "@/components/datatable" +import DatatableSales from "@/components/datatableSales" +import Sales2Skeleton from "@/components/skeleton/Sales2Skeleton" +import TotalSales from "@/components/superAdmin/TotalSales" + +export default function Sales({ params }) { + const name = params.name + const [userDetails, setuserDetails] = useState() + const [userData, setuserData] = useState([]) + const [data, setData] = useState(null) + const [projects, setprojects] = useState(0) + const [clients, setclients] = useState(0) + const [earnings, setearnings] = useState(0) + const [seller1earnings, setseller1earnings] = useState(0) + const [commission, setcommission] = useState(0) + const [seller1commission, setSeller1commission] = useState(0) + const [grandCommission, setgrandCommission] = useState(0) + const [AllCompanies, setAllCompanies] = useState([]) + + useEffect(() => { + fetch(`/api/user?name=${name}`) + .then((res) => res.json()) + .then((user) => { + setuserDetails(user?.users) + setAllCompanies(user?.users?.contracts) + console.log(user?.users) + //=============================fetch user data + fetchSales(user?.users?._id) + .then((userData) => { + setuserData(userData) + }) + .catch((error) => { + console.error("Error in component:", error) + }) + fetchSales(user.users?._id) + .then((userData) => { + setuserData(userData) + }) + .catch((error) => { + console.error("Error in component:", error) + }) + //========================================== stat + fetch(`/api/stat?id=${user.users?._id}`) + .then((res) => res.json()) + .then((data) => { + setprojects(data.projects) + setclients(data.clients) + setearnings(data.totalAmount) + setcommission(data.commission) + }) + .catch((error) => { + console.error("Error:", error) + }) + //============================================= commission + fetch(`/api/commission?upsale=${user.users?._id}`) + .then((res) => res.json()) + .then((data) => { + setseller1earnings(data.totalEarnings) + setSeller1commission(data.commission) + }) + .catch((error) => { + console.error("Error:", error) + }) + }) + .catch((error) => { + console.error("Error:", error) + }) + fetchProjects() + .then((apiData) => { + if (apiData) { + const filteredData = apiData.filter( + (item) => item.salesPerson == decodeURIComponent(name) + ) + setData(filteredData) + } + }) + .catch((error) => { + console.error("Error in component:", error) + }) + }, [name]) + return ( + <> +
+
+
+ {userDetails && ( + <> +
+
+
+ {userDetails?.avatar ? ( + {"hello"} + ) : ( + {"hello"} + )} +
+
+

+ {userDetails?.name} +

+
{userDetails?.email}
+
2193-13102939
+ {userDetails?.role == "Sales1" && ( +
+ + Level One Seller +
+ )} + {userDetails?.role == "Sales2" && ( +
+ + Level Two Seller +
+ )} +
+
+
+
+ +
+ {userDetails && + userDetails?.role == "Sales2" && + userData && + data && ( + <> +
+

Earnings

+

+ ${commission} +

+
+ {/*
+

Commission

+

+ {commission} +

+
*/} +
+

Clients

+

+ {clients} +

+
+
+

Projects

+

+ {projects} +

+
+ + )} + + {userDetails && userDetails?.role == "Sales1" && ( + <> +
+

Sales Reps

+

+ +

+
+
+

Earnings

+

+ ${seller1earnings ? seller1earnings.toFixed(2) : 0} +

+
+
+

Commission Earnings

+

+ $ + {seller1commission + ? seller1commission.toFixed(2) + : 0} +

+
+ + )} +
+
+ + )} +
+
+ + {!userDetails && !data && ( +
+ +
+ )} + {userDetails && userDetails.role == "Sales1" && userData && ( + <> +

Projects

+ +

Sales Rep

+ + + )} + {userDetails && userDetails.role == "Sales2" && data && ( + + )} +
+ + ) +} diff --git a/app/settings/manage-clients/page.tsx b/app/settings/manage-clients/page.tsx new file mode 100644 index 0000000..0cd6f91 --- /dev/null +++ b/app/settings/manage-clients/page.tsx @@ -0,0 +1,325 @@ +"use client" + +import React, { useEffect, useState } from "react" +import Link from "next/link" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { FileEdit, Plus, Trash2,User,Search } from "lucide-react" + +import { fetchClients } from "@/lib/fetchClients" +import MangeUser from "@/components/skeleton/MangeUser" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import EditClient from "@/components/superAdmin/EditClient" +import ViewClient from "@/components/superAdmin/ViewClient" + +const columns = [ + + { + accessorKey: "clientName", + header: ({ column }) => { + return ( + + ) + }, + }, + { + accessorKey: "email", + header: "Email", + cell: ({ row }) =>
{row.getValue("email")}
, + }, + { + accessorKey: "phone", + header: ({ column }) => { + return ( + + ) + }, + }, + + { + accessorKey: "action", + header: "Action", + cell: ({ row }) => ( +
+
+ +
+
+ +
+
+ + +
+ +
+
+ + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the + project and remove data from our servers. + + + + Cancel + { + const res = await fetch( + `/api/client?id=${row.original._id}`, + { + method: "DELETE", + } + ) + if (res.ok) { + window.location.reload() + } + }} + > + Remove + + + +
+
+
+ ), + }, +] + +export default function ManageClient() { + const [data, setData] = useState(null) + const [isLoading, setisLoading] = useState(true) + + useEffect(() => { + fetchClients("SuperAdmin") + .then((apiData) => { + console.log(apiData) + setData(apiData) + setisLoading(false) + }) + .catch((error) => { + console.error("Error in component:", error) + }) + }, []) + + const [role, setRole] = useState() + + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + if (isLoading) { + return ( +
+ +
+ ) + } + + return ( +
+

Manage Clients

+
+
+ + + + table.getColumn("clientName")?.setFilterValue(event.target.value) + } + className="w-full border-0 rounded-none" + /> +
+ +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <> + + {row.getVisibleCells().map((cell) => ( + <> + + + {cell.column.id=="clientName"?
:""} + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+ + ))} +
+ + )) + ) : ( + + + No results. + + + )} +
+
+
+
+
+
+ + +
+
+
+ ) +} diff --git a/app/settings/manage-companies/AddCompany.tsx b/app/settings/manage-companies/AddCompany.tsx new file mode 100644 index 0000000..c9697e5 --- /dev/null +++ b/app/settings/manage-companies/AddCompany.tsx @@ -0,0 +1,274 @@ +"use client" + +import React, { useState } from "react" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { ScrollArea } from "@/components/ui/scroll-area" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet" +import { Textarea } from "@/components/ui/textarea" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" +import { Loader } from "lucide-react" + +export default function Addcompany() { + const { toast } = useToast() + + const [Loading, setLoading] = useState(false) + const [uploading, setUploading] = useState(false) + const [companyLogo, setcompanyLogo] = useState() + const [companyName, setcompanyName] = useState() + const [companyType, setcompanyType] = useState() + const [companyAddress, setcompanyAddress] = useState() + const [companyEmail, setcompanyEmail] = useState() + const [companyPhone, setcompanyPhone] = useState() + const [socialLink, setSocialLink] = useState({ + facebook:"", + twitter:"", + instagram:"", + linkedin:"" + }) + console.log(socialLink) + const [overview, setOverview] = useState() + const [rate, setRate] = useState() + const [imagePreview, setimagePreview] = useState("") + const [image, setImage] = useState("") + + const handleImageChange = (e) => { + const file = e.target.files[0] + setimagePreview(URL.createObjectURL(file)) + setImage(file) + } + +const handleImageUpload=async(e)=>{ + e.preventDefault() + if (image) { + const formData = new FormData() + formData.append("file", image) + formData.append("upload_preset", "p2y46g7e") + try { + setLoading(true) + const response = await fetch( + `https://api.cloudinary.com/v1_1/drzedrk1e/image/upload`, + { + method: "POST", + body: formData, + } + ) + if (response.ok) { + const data = await response.json() + handleSubmit(data.url) + } else { + console.error("Error uploading image:", response.statusText) + } + } catch (error) { + console.error("Error uploading image:", error) + } +} +} + + const handleSubmit = async(data) => { + if ( + !data|| + !companyType || + !rate || + !companyAddress || + !companyEmail || + !companyPhone || + !overview + ) { + console.log("all fields required") + toast({ + variant: "destructive", + title: "all fields required", + }) + setLoading(false) + return + }else{ + try { + const res = await fetch("../api/company", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + companyLogo:data, + companyName, + companyType, + companyAddress, + companyEmail, + companyPhone, + overview, + rate, + socialLink + }), + }) + + if (res.ok) { + setLoading(false) + console.log("company added!") + toast({ + title: "company added", + }) + window.location.reload() + } else { + console.log("company submit failed!") + toast({ + variant: "destructive", + title: "company submit failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during company submit:", error) + toast({ + variant: "destructive", + title: `Error during company submit:", ${error}`, + }) + setLoading(false) + } + } + + } + + function convertToLowerCase(inputString) { + return inputString.toLowerCase() + } +//===============handle social link +const handleSocialLink = (e) => { + const { name, value } = e.target; + setSocialLink(prevState => ({ + ...prevState, + [name]: value + })); +}; + return ( + + + + Add Company + + + + + + +

Add Company

+
+ +
+
+ + + + + + setcompanyName(convertToLowerCase(e.target.value.trim())) + } + /> +
+
+ + setcompanyType(e.target.value.trim())} + /> +
+
+ + setcompanyAddress(e.target.value.trim())} + /> +
+
+ + setRate(e.target.value.trim())} + /> +
+
+ + setcompanyEmail(e.target.value.trim())} + /> +
+
+ + setcompanyPhone(e.target.value.trim())} + /> +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+
+
+
+
+ ) +} diff --git a/app/settings/manage-companies/Datatable.tsx b/app/settings/manage-companies/Datatable.tsx new file mode 100644 index 0000000..cff34e1 --- /dev/null +++ b/app/settings/manage-companies/Datatable.tsx @@ -0,0 +1,223 @@ +"use client" +import { fetchCompanies } from "@/lib/company/company" + +import React, { useEffect, useState } from "react" +import Image from "next/image" +import Link from "next/link" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { Trash2,Search } from "lucide-react" + +import { fetchCompany } from "@/lib/company/company" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import Addcompany from "./AddCompany" +import DeleteCompany from "./DeleteCompany" +import EditCompany from "./EditCompany" + +function replaceSpaceAndLowerCase(inputString) { + const result = inputString.replace(/ /g, "-").toLowerCase() + return result +} + +export const columns = [ + { + accessorKey: "companyLogo", + header: "Company Logo", + cell: ({ row }) => ( +
+ + logo + +
+ ), + }, + + { + accessorKey: "companyName", + header: "Company Name", + + cell: ({ row }) => ( +
+ + {row.getValue("companyName")} + +
+ ), + }, + { + accessorKey: "companyEmail", + header: "Company Email", + cell: ({ row }) =>
{row.getValue("companyEmail")}
, + }, + { + accessorKey: "_id", + header: "Actions", + cell: ({ row }) => ( +
+ + +
+ ), + }, +] + +export default function DataTableDemo(props) { + +const [data,setData]= useState([]) + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + const fetchCompany=async()=>{ + const data = await fetchCompanies() + console.log(data) + setData(data) + } + useEffect(()=>{ + fetchCompany() + },[]) + return ( +
+
+
+ + + + table.getColumn("companyName")?.setFilterValue(event.target.value) + } + className="w-full border-0 rounded-none" + /> +
+
+ +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ + +
+
+
+ ) +} diff --git a/app/settings/manage-companies/DeleteCompany.tsx b/app/settings/manage-companies/DeleteCompany.tsx new file mode 100644 index 0000000..1956dfe --- /dev/null +++ b/app/settings/manage-companies/DeleteCompany.tsx @@ -0,0 +1,59 @@ +"use client" + +import React, { useState } from "react" +import { Trash2 } from "lucide-react" + +import { deleteCompany } from "@/lib/company/company" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Icons } from "@/components/icons" + +export default function DeleteCompany(props) { + const id = props.id + const [Loading, setLoading] = useState(false) + + const deleteFunction = () => { + setLoading(true) + deleteCompany(id).then((res) => { + setLoading(false) + window.location.reload() + }) + } + + return ( +
+ + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + Cancel + + {Loading && ( + + )} + Continue + + + + +
+ ) +} diff --git a/app/settings/manage-companies/EditCompany.tsx b/app/settings/manage-companies/EditCompany.tsx new file mode 100644 index 0000000..04e42a1 --- /dev/null +++ b/app/settings/manage-companies/EditCompany.tsx @@ -0,0 +1,337 @@ +"use client" + +import React, { useEffect, useState } from "react" +import { + Calendar as CalendarIcon, + Check, + ChevronsUpDown, + FileEdit, + PlusCircle, +} from "lucide-react" + +import { fetchCompanies } from "@/lib/company/company" +import { getBaseUrl } from "@/lib/getBaseUrl" +import { cn } from "@/lib/utils" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +import { Button } from "../../../components/ui/button" +import { fetchUsers } from "../../../lib/fetchUsers" + +export default function EditCompany(props) { + const [open, setOpen] = useState(false) + const [value, setValue] = useState("") + const companyId = props.id + const { toast } = useToast() + const [Loading, setLoading] = useState(false) + const [companyLogo, setcompanyLogo] = useState() + const [companyName, setcompanyName] = useState() + const [companyType, setcompanyType] = useState() + const [companyAddress, setcompanyAddress] = useState() + const [companyEmail, setcompanyEmail] = useState() + const [companyPhone, setcompanyPhone] = useState() + const [overview, setOverview] = useState() + const [rate, setRate] = useState() + const [imagePreview, setimagePreview] = useState("") + const [image, setImage] = useState("") + const [company, setCompany] = useState() + const [socialLink, setSocialLink] = useState({ + facebook: "", + twitter: "", + instagram: "", + linkedin: "", + }) + console.log(socialLink) + const handleSocialLink = (e) => { + const { name, value } = e.target + setSocialLink((prevState) => ({ + ...prevState, + [name]: value, + })) + } + + useEffect(() => { + fetch(`/api/company?id=${companyId}`) + .then((response) => response.json()) + .then((data) => { + setcompanyName(data.company.companyName) + setcompanyType(data.company.companyType) + setcompanyAddress(data.company.companyAddress) + setcompanyEmail(data.company.companyEmail) + setcompanyPhone(data.company.companyPhone) + setOverview(data.company.overview) + setRate(data.company.rate) + setSocialLink(data.company.socialLink) + setimagePreview(data.company.companyLogo) + }) + .catch((error) => { + console.error("Error:", error) + }) + }, [props]) + + const handleImageChange = (e) => { + const file = e.target.files[0] + setimagePreview(URL.createObjectURL(file)) + setImage(file) + } + + const handleImageUpload = async (e) => { + e.preventDefault() + + if (image) { + const formData = new FormData() + formData.append("file", image) + formData.append("upload_preset", "p2y46g7e") + try { + setLoading(true) + const response = await fetch( + `https://api.cloudinary.com/v1_1/drzedrk1e/image/upload`, + { + method: "POST", + body: formData, + } + ) + if (response.ok) { + const data = await response.json() + handleEidtSubmit(data.url) + } else { + console.error("Error uploading image:", response.statusText) + } + } catch (error) { + console.error("Error uploading image:", error) + } + } else { + handleEidtSubmit(null) + } + } + const handleEidtSubmit = async (data) => { + setLoading(true) + try { + const res = await fetch(`/api/company?id=${companyId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + companyLogo: data, + companyName, + companyType, + companyAddress, + companyEmail, + companyPhone, + overview, + rate, + socialLink, + }), + }) + + if (res.ok) { + setLoading(false) + console.log("company updated!") + toast({ + title: "company updated", + }) + window.location.reload() + } else { + console.log("company submit failed!") + toast({ + variant: "destructive", + title: "company submit failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during company submit:", error) + toast({ + variant: "destructive", + title: `Error during company submit:", ${error}`, + }) + setLoading(false) + } + } + function convertToLowerCase(inputString) { + return inputString.toLowerCase() + } + return ( + + + + + + + Edit Company + +
+
+ +
+ + +
+
+
+
+ + + setcompanyName( + convertToLowerCase(e.target.value.trim()) + ) + } + /> +
+
+ + setcompanyType(e.target.value.trim())} + /> +
+
+ + setcompanyAddress(e.target.value.trim())} + /> +
+
+ + setRate(e.target.value.trim())} + /> +
+
+ + setcompanyEmail(e.target.value.trim())} + /> +
+
+ + setcompanyPhone(e.target.value.trim())} + /> +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+ +
+
+
+
+
+
+ ) +} + diff --git a/app/settings/manage-companies/page.tsx b/app/settings/manage-companies/page.tsx new file mode 100644 index 0000000..356c37a --- /dev/null +++ b/app/settings/manage-companies/page.tsx @@ -0,0 +1,8 @@ +import React from "react" + +import DataTable from "./Datatable" + +export default async function ManageCompanyPage() { + + return
+} diff --git a/app/settings/manage-companies/untitled folder/AddCompany.tsx b/app/settings/manage-companies/untitled folder/AddCompany.tsx new file mode 100644 index 0000000..b505bc8 --- /dev/null +++ b/app/settings/manage-companies/untitled folder/AddCompany.tsx @@ -0,0 +1,232 @@ +"use client" + +import React, { useState } from "react" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { ScrollArea } from "@/components/ui/scroll-area" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet" +import { Textarea } from "@/components/ui/textarea" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" +import { Loader } from "lucide-react" + +export default function Addcompany() { + const { toast } = useToast() + + const [Loading, setLoading] = useState(false) + const [uploading, setUploading] = useState(false) + const [companyLogo, setcompanyLogo] = useState() + const [companyName, setcompanyName] = useState() + const [companyType, setcompanyType] = useState() + const [companyAddress, setcompanyAddress] = useState() + const [companyEmail, setcompanyEmail] = useState() + const [companyPhone, setcompanyPhone] = useState() + const [overview, setOverview] = useState() + const [rate, setRate] = useState() + const [imagePreview, setimagePreview] = useState("") + const [image, setImage] = useState("") + + const handleImageChange = (e) => { + const file = e.target.files[0] + setimagePreview(URL.createObjectURL(file)) + setImage(file) + } + +const handleImageUpload=async(e)=>{ + e.preventDefault() + if (image) { + const formData = new FormData() + formData.append("file", image) + formData.append("upload_preset", "p2y46g7e") + try { + setLoading(true) + const response = await fetch( + `https://api.cloudinary.com/v1_1/drzedrk1e/image/upload`, + { + method: "POST", + body: formData, + } + ) + if (response.ok) { + const data = await response.json() + handleSubmit(data.url) + } else { + console.error("Error uploading image:", response.statusText) + } + } catch (error) { + console.error("Error uploading image:", error) + } +} +} + + const handleSubmit = async(data) => { + if ( + !data|| + !companyType || + !rate || + !companyAddress || + !companyEmail || + !companyPhone || + !overview + ) { + console.log("all fields required") + toast({ + variant: "destructive", + title: "all fields required", + }) + setLoading(false) + return + }else{ + try { + const res = await fetch("../api/company", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + companyLogo:data, + companyName, + companyType, + companyAddress, + companyEmail, + companyPhone, + overview, + rate, + }), + }) + + if (res.ok) { + setLoading(false) + console.log("company added!") + toast({ + title: "company added", + }) + window.location.reload() + } else { + console.log("company submit failed!") + toast({ + variant: "destructive", + title: "company submit failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during company submit:", error) + toast({ + variant: "destructive", + title: `Error during company submit:", ${error}`, + }) + setLoading(false) + } + } + + } + + function convertToLowerCase(inputString) { + return inputString.toLowerCase() + } + + return ( + + + + Add Company + + + + + + +

Add Company

+
+ +
+
+ + + + + + setcompanyName(convertToLowerCase(e.target.value.trim())) + } + /> +
+
+ + setcompanyType(e.target.value.trim())} + /> +
+
+ + setcompanyAddress(e.target.value.trim())} + /> +
+
+ + setRate(e.target.value.trim())} + /> +
+
+ + setcompanyEmail(e.target.value.trim())} + /> +
+
+ + setcompanyPhone(e.target.value.trim())} + /> +
+
+ + +
+ +
+
+
+
+
+
+ ) +} diff --git a/app/settings/manage-companies/untitled folder/Datatable.tsx b/app/settings/manage-companies/untitled folder/Datatable.tsx new file mode 100644 index 0000000..0b3a3cf --- /dev/null +++ b/app/settings/manage-companies/untitled folder/Datatable.tsx @@ -0,0 +1,207 @@ +"use client" + +import React, { useEffect, useState } from "react" +import Image from "next/image" +import Link from "next/link" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { Trash2 } from "lucide-react" + +import { fetchCompany } from "@/lib/company/company" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import Addcompany from "./AddCompany" +import DeleteCompany from "./DeleteCompany" + +function replaceSpaceAndLowerCase(inputString) { + const result = inputString.replace(/ /g, "-").toLowerCase() + return result +} + +export const columns = [ + { + accessorKey: "companyLogo", + header: "Company Logo", + cell: ({ row }) => ( +
+ + logo + +
+ ), + }, + { + accessorKey: "companyName", + header: "Company Name", + cell: ({ row }) => ( +
+ + {row.getValue("companyName")} + +
+ ), + }, + { + accessorKey: "companyEmail", + header: "Company Email", + cell: ({ row }) =>
{row.getValue("companyEmail")}
, + }, + { + accessorKey: "_id", + header: "Actions", + cell: ({ row }) => ( +
+ +
+ ), + }, +] + +export default function DataTableDemo(props) { + const data = props.data + + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + return ( +
+
+
+ + table.getColumn("companyName")?.setFilterValue(event.target.value) + } + className="w-full" + /> +
+
+ +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ + +
+
+
+ ) +} diff --git a/app/settings/manage-contracts/AddContract.tsx b/app/settings/manage-contracts/AddContract.tsx new file mode 100644 index 0000000..7f3513e --- /dev/null +++ b/app/settings/manage-contracts/AddContract.tsx @@ -0,0 +1,169 @@ +"use client" + +import React, { useEffect, useState } from "react" +import { Plus } from "lucide-react" + +import { fetchCompanies } from "@/lib/company/company" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { toast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +export default function AddContract(props) { + const userId = props.id + const [Loading, setLoading] = useState(false) + const [userData, setuserData] = useState() + + const [activeCompany, setactiveCompany] = useState() + const [offerCompany, setofferCompany] = useState() + + const [NewCompany, setNewCompany] = useState() + const [selectedRate, setSelectedRate] = useState(null) + const [selectedLogo, setSelectedLogo] = useState(null) + + useEffect(() => { + fetch(`/api/user/?id=${userId}`) + .then((response) => response.json()) + .then((data) => { + const userData = data.user + setuserData(userData) + const companyNamesArray = userData.contracts.map( + (contract) => contract.companyName + ) + setactiveCompany(companyNamesArray) + }) + .catch((error) => { + console.error("Error:", error) + }) + + fetchCompanies().then((companies) => { + setofferCompany(companies) + }) + }, []) + + const handleCompanyChange = (selectedCompany) => { + setNewCompany(selectedCompany) + const selectedCompanyObj = offerCompany.find( + (company) => company.companyName === selectedCompany + ) + if (selectedCompanyObj) { + setSelectedRate(selectedCompanyObj.rate) + setSelectedLogo(selectedCompanyObj.companyLogo) + } + } + + const handleSubmit = async (e) => { + e.preventDefault() + setLoading(true) + + try { + const newContract = { + companyName: NewCompany, + rate: selectedRate, + logo: selectedLogo, + } + + const updatedUser = { + contracts: [...userData.contracts, newContract], + } + + const res = await fetch(`/api/user/?id=${userId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updatedUser), + }) + + if (res.status === 200 || res.status === 201) { + setLoading(false) + toast({ + variant: "default", + title: "User Updated!", + }) + window.location.reload() + } else { + console.log("Submission failed!") + toast({ + title: `Submission failed! Status: ${res.status}`, + }) + setLoading(false) + } + } catch (error) { + console.log("Error during submit:", error) + toast({ + variant: "destructive", + title: `Error during submit: ${error.message}`, + }) + setLoading(false) + } + } + + return ( + + + + + + + Add Contract + +
+ + + + +
+
+
+ ) +} diff --git a/app/settings/manage-contracts/DeleteContract.tsx b/app/settings/manage-contracts/DeleteContract.tsx new file mode 100644 index 0000000..cae10c2 --- /dev/null +++ b/app/settings/manage-contracts/DeleteContract.tsx @@ -0,0 +1,110 @@ +"use client" +import React,{useState} from "react"; +import { Pencil,Trash ,Trash2} from 'lucide-react'; +import { toast } from "@/components/ui/use-toast" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" + +const DeleteContract = (props) => { + const id =props?.id + const contract =props?.contract + const allUser =props?.allUser + const [Loading, setLoading] = useState(false) + const handleDelete = async ( ) => { + setLoading(true); + try { + const res = await fetch(`/api/user/?id=${id}`); + const userData = await res.json(); + + const updatedContracts = userData?.user?.contracts.filter((item) => item._id !== contract._id); + const updatedUserData = { + ...userData, + contracts: updatedContracts + }; + const updateRes = await fetch(`/api/user/?id=${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updatedUserData), + }); + + if (updateRes.status === 200 || updateRes.status === 201) { + setLoading(false); + toast({ + variant: "default", + title: "Contract deleted!", + }); + window.location.reload(); + } else { + console.log("Delete failed!"); + toast({ + title: `Delete failed! Status: ${updateRes.status}`, + }); + setLoading(false); + } + } catch (error) { + console.log("Error during submit:", error); + toast({ + variant: "destructive", + title: `Error during submit: ${error.message}`, + }); + setLoading(false); + } + }; + return
+ + + + + + + + + + + + Are you absolutely sure? + + + This action cannot be undone. This will permanently + delete the contact and remove data from our servers. + + + + Cancel + + Remove + + + + + + +

Delete

+
+
+
+ +
; +}; + +export default DeleteContract; diff --git a/app/settings/manage-contracts/EditContract.tsx b/app/settings/manage-contracts/EditContract.tsx new file mode 100644 index 0000000..d46afb7 --- /dev/null +++ b/app/settings/manage-contracts/EditContract.tsx @@ -0,0 +1,182 @@ +"use client" + +import React, { useEffect, useState } from "react" +import { FilePlus2, Pencil } from "lucide-react" + +import { fetchCompanies } from "@/lib/company/company" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { toast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +export default function EditContract(props) { + const userId = props.id + const contract = props.contract + const [Loading, setLoading] = useState(false) + const [userData, setuserData] = useState() + + const [activeCompany, setactiveCompany] = useState() + const [offerCompany, setofferCompany] = useState() + + const [NewCompany, setNewCompany] = useState() + const [selectedRate, setSelectedRate] = useState(null) + const [selectedLogo, setSelectedLogo] = useState(null) + + useEffect(() => { + fetch(`/api/user/?id=${userId}`) + .then((response) => response.json()) + .then((data) => { + const userData = data.user + setuserData(userData) + const companyNamesArray = userData.contracts.map( + (contract) => contract.companyName + ) + setactiveCompany(companyNamesArray) + }) + .catch((error) => { + console.error("Error:", error) + }) + + fetchCompanies().then((companies) => { + setofferCompany(companies) + }) + }, []) + + const handleCompanyChange = (selectedCompany) => { + setNewCompany(selectedCompany) + const selectedCompanyObj = offerCompany.find( + (company) => company.companyName === selectedCompany + ) + if (selectedCompanyObj) { + setSelectedRate(selectedCompanyObj.rate) + setSelectedLogo(selectedCompanyObj.companyLogo) + } + } + + useEffect(() => { + setNewCompany(contract.companyName) + setSelectedRate(contract.rate) + setSelectedLogo(contract.companyLogo) + },[]) + const handleSubmit = async (e) => { + e.preventDefault() + setLoading(true) + + try { + const response = await fetch(`/api/user/?id=${userId}`) + const userData = await response.json() + + const updatedContracts = userData?.user?.contracts.filter( + (item) => item._id !== contract._id + ) + const newContract = { + companyName: NewCompany, + rate: selectedRate, + logo: selectedLogo, + } + const allContracts = updatedContracts + ? [...updatedContracts, newContract] + : [newContract] + const updatedUser = { + ...userData, + contracts: allContracts, + } + + const res = await fetch(`/api/user/?id=${userId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updatedUser), + }) + + if (res.status === 200 || res.status === 201) { + setLoading(false) + toast({ + variant: "default", + title: "User Updated!", + }) + window.location.reload() + } else { + console.log("Submission failed!") + toast({ + title: `Submission failed! Status: ${res.status}`, + }) + setLoading(false) + } + } catch (error) { + console.log("Error during submit:", error) + toast({ + variant: "destructive", + title: `Error during submit: ${error.message}`, + }) + setLoading(false) + } + } + + return ( + + + + + + + Add Contract + +
+ + + + +
+
+
+ ) +} diff --git a/app/settings/manage-contracts/page.tsx b/app/settings/manage-contracts/page.tsx new file mode 100644 index 0000000..3791f79 --- /dev/null +++ b/app/settings/manage-contracts/page.tsx @@ -0,0 +1,12 @@ +import React from "react" + +import ContractTable from "../../../components/ContractTable.jsx" + +export default async function ManageContracts() { + + return ( +
+ +
+ ) +} diff --git a/app/settings/manage-meetings/page.tsx b/app/settings/manage-meetings/page.tsx new file mode 100644 index 0000000..d2cb136 --- /dev/null +++ b/app/settings/manage-meetings/page.tsx @@ -0,0 +1,14 @@ +import React from "react" + +import AddMeeting from "@/components/meetings/AddMeeting" +import MeetingList from "@/components/meetings/MeetingList" + +export default function page() { + return ( +
+

Manage Meetings

+ + +
+ ) +} diff --git a/app/settings/manage-user/page.tsx b/app/settings/manage-user/page.tsx new file mode 100644 index 0000000..556b4eb --- /dev/null +++ b/app/settings/manage-user/page.tsx @@ -0,0 +1,373 @@ +"use client" + +import React, { useEffect, useState } from "react" +import Link from "next/link" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { FileEdit, Plus, Trash2 ,Search,User} from "lucide-react" + +import { fetchAllUsers } from "@/lib/fetchUsers" +import MangeUser from "@/components/skeleton/MangeUser" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import EditUser from "@/components/superAdmin/EditUser" + +const columns = [ + { + accessorKey: "name", + header: "Name", + cell: ({ row }) => ( +
+ + + + {row.getValue("name")} + +
+ ), + }, + { + accessorKey: "email", + header: "Email", + cell: ({ row }) =>
{row.getValue("email")}
, + }, + { + accessorKey: "role", + header: ({ column }) => { + return ( + + ) + }, + }, + // { + // accessorKey: "commission_rate", + // header: ({ column }) => { + // return ( + // + // ) + // }, + // }, + { + accessorKey: "upSeller", + header: ({ column }) => { + return ( + + ) + }, + }, + { + accessorKey: "action", + header: "Action", + cell: ({ row }) => ( +
+
+ +
+
+ + +
+ +
+
+ + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the + project and remove data from our servers. + + + + Cancel + { + const res = await fetch( + `/api/user?id=${row.original._id}`, + { + method: "DELETE", + } + ) + if (res.ok) { + window.location.reload() + } + }} + > + Remove + + + +
+
+
+ ), + }, +] + +export default function ManageUser() { + const [data, setData] = useState(null) + + const [isLoading, setisLoading] = useState(true) + useEffect(() => { + fetchAllUsers("SuperAdmin") + .then((apiData) => { + setData(apiData) + setisLoading(false) + }) + .catch((error) => { + console.error("Error in component:", error) + }) + }, []) + + const [role, setRole] = useState() + + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + if (isLoading) { + return ( +
+ +
+ ) + } + + return ( +
+

Manage Users

+
+
+ + + table.getColumn("name")?.setFilterValue(event.target.value) + } + className="w-full border-0 rounded-none" + /> +
+
+
+ +
+
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + <> + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+
+ + +
+
+
+ ) +} diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 0000000..3db78bf --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,276 @@ +"use client" + +import React, { useEffect, useState } from "react" +import { ResponsiveBar } from "@nivo/bar" +import { Copy, Lock, Mail, RotateCcw } from "lucide-react" +import { useSession } from "next-auth/react" + +import { getUser } from "@/lib/getUser" +import { + sendAdminNotification, + sendUserNotification, +} from "@/lib/notification/sendNotification" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" +import { Skeleton } from "@/components/ui/skeleton" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" +import ClipboardCopy from "@/components/settings/ClipboardCopy" +import ProfileUpload from "@/components/settings/ProfileUpload" + +export default function Settings() { + const { data: session, update } = useSession() + const id = session?.user?.id + + const [avatar, setAvatar] = useState() + const [name, setName] = useState("") + const [isLoading, setLoading] = useState(false) + const { toast } = useToast() + useEffect(() => { + getUser(id) + .then((userData) => { + setAvatar(userData.avatar) + }) + .catch((error) => { + console.error("Error in component:", error) + }) + }, [session, id]) + //=========================reset password + const handleResetPassword = () => { + if (session) { + sendAdminNotification("", session?.user?.name, session?.user?.email) + toast({ + variant: "default", + title: "Check your Email!", + }) + } + } + const handleUpdate = async () => { + await update({ + ...session, + user: { + ...session?.user, + name: name, + }, + }) + } + //=========================update full name. + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + if (!name) { + toast({ + variant: "destructive", + title: "Name field is required!", + }) + } else { + setLoading(true) + + try { + const res = await fetch(`/api/user/?id=${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + }), + }) + + if (res.ok) { + await handleUpdate() + setLoading(false) + window.location.reload() + toast({ + variant: "default", + title: "Your Name Updated!", + }) + } else { + console.log("submission failed!") + toast({ + title: "submission failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during submit:", error) + toast({ + variant: "destructive", + title: `Error during submit:", ${error}`, + }) + setLoading(false) + } + } + } + return ( + <> +
+
+

Settings

+

Manage your account settings from here.

+ + + + + Profile + + + Reset password + + + Referral Link + + + +
+
+

Profile

+

+ Change your profile settings +

+
+
+ {!avatar && ( + + )} + {avatar && ( + profile image + )} + +
+
+
+
+ + setName(e.target.value)} + type="text" + placeholder="Full Name" + /> +
+ +
+
+
+
+ +
+
+ +
+ + +
+
+ + +
+

+ Copy & Share Your Referral Link +

+ +
+ Total Signup +

120

+
+ + +
+ `${id}`} + enableLabel={false} + role="application" + ariaLabel="A bar chart showing data" + /> +
+
+
+
+
+
+
+
+ + ) +} diff --git a/app/signup/page.tsx b/app/signup/page.tsx new file mode 100644 index 0000000..3c401dc --- /dev/null +++ b/app/signup/page.tsx @@ -0,0 +1,59 @@ +"use client" + +import { Suspense, useEffect } from "react" +import { Metadata } from "next" +import Image from "next/image" +import Link from "next/link" +import { redirect, useSearchParams } from "next/navigation" +import cookie from "js-cookie" +import { useSession } from "next-auth/react" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" +import ReferralComp from "@/components/ReferralComp" +import { Signinform } from "@/components/Signinform" + +export default function SignupPage() { + const { data: session } = useSession() + if (session) redirect("/dashboard") + + return ( + <> + Loading...
}> + + +
+ + Login + +
+
+
+ + Logo + +
+
+
+
+
+

+ Create an Account +

+

+ Enter your details below to create your account +

+
+ +
+
+
+ + ) +} diff --git a/app/training/ListComponent.tsx b/app/training/ListComponent.tsx new file mode 100644 index 0000000..57d643e --- /dev/null +++ b/app/training/ListComponent.tsx @@ -0,0 +1,74 @@ +"use client" + +import React, { useEffect, useRef, useState } from "react" +import Link from "next/link" + +const ListComponent = (props: any) => { + const highLightRef = useRef() + + useEffect(() => { + const handleScroll = () => { + const rect = highLightRef?.current?.getBoundingClientRect() + const isVisible = rect?.top >= 0 && rect?.bottom <= window.innerHeight + if (isVisible) { + props.setActive(highLightRef?.current?.getAttribute("id")) + } + } + window.addEventListener("scroll", handleScroll) + return () => { + window.removeEventListener("scroll", handleScroll) + } + }, [highLightRef]) + + return ( +
+ {/* {props.level && ( +

+ {props.level} +

+ )} */} + +
    +
  1. + + {props.id}. + {props.title} + +
    + +
    +
      +
    • + + Importance : + {" "} + {props.importance} +
    • +
    • + + Action Steps: + +
        + {props.actionSteps?.map((item, index) => { + return ( +
      • + {item} +
      • + ) + })} +
      +
    • +
    +
  2. +
+
+ ) +} + +export default ListComponent diff --git a/app/training/page.tsx b/app/training/page.tsx new file mode 100644 index 0000000..824b6c2 --- /dev/null +++ b/app/training/page.tsx @@ -0,0 +1,222 @@ +"use client" + +import React, { useEffect, useState } from "react" +import Link from "next/link" +import { ChevronUp , ChevronRight } from "lucide-react" + +import ListComponent from "./ListComponent" + +export default function TrainingPage() { + const salesSkillsArray = [ + // Beginner Level + { + level: "Beginner Level: Building Strong Foundations", + title: "The Power of Connection", + video: "https://www.youtube.com/embed/yyJm5IRYGgE?si=p40eMbmMEL9a-5si", + importance: "Genuine connections are the bedrock of successful sales.", + actionSteps: [ + "Attend team-building events and get to know your colleagues.", + "Practice active listening during conversations with clients and team members.", + ], + }, + { + level: "", + title: "Networking 101", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: + "Expanding your professional network opens doors to opportunities.", + actionSteps: [ + "Attend industry events and engage with fellow professionals.", + "Utilize LinkedIn to connect with potential clients and industry leaders.", + ], + }, + { + level: "", + title: "Celebrate Small Wins", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: "Recognizing achievements boosts morale and motivation.", + actionSteps: [ + "Share successes, no matter how small, during team meetings.", + "Foster a positive team culture by acknowledging individual and team accomplishments.", + ], + }, + // inter midiate Level + { + level: "Intermediate Level: Elevating Performance", + title: "Deep Dive into Product Knowledge", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: + "Knowing your product inside out builds confidence and credibility.", + actionSteps: [ + "Participate in product training sessions and workshops.", + "Continuously update your knowledge as products evolve.", + ], + }, + { + level: "", + title: "Master the Art of Presentation", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: + "A compelling presentation showcases the value of your product.", + actionSteps: [ + "Practice delivering polished and engaging presentations.", + "Seek feedback from colleagues and mentors to refine your presentation skills.", + ], + }, + { + level: "", + title: "Sharpen Negotiation Skills", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: + "Effective negotiation leads to mutually beneficial outcomes.", + actionSteps: [ + "Study negotiation techniques and strategies.", + "Role-play scenarios to hone your negotiation skills.", + ], + }, + { + level: "", + title: "Collaborate for Success", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: "Team collaboration amplifies overall performance.", + actionSteps: [ + "Actively participate in team projects and initiatives.", + "Foster a collaborative environment by sharing insights and best practices.", + ], + }, + // advance Level + { + level: "Advanced Level: Mastery and Leadership", + title: "Embrace Strategic Thinking", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: "Strategic planning guides long-term success.", + actionSteps: [ + "Analyze market trends and competitor strategies.", + "Contribute strategic insights during team discussions.", + ], + }, + { + level: "", + title: "Executive-Level Communication", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: + "Effectively communicate with decision-makers for impactful sales.", + actionSteps: [ + "Craft clear and concise messages tailored to executive audiences.", + "Seek mentorship on executive communication skills.", + ], + }, + { + level: "", + title: "Mentorship and Leadership", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: + "Elevate others and contribute to a culture of continuous learning.", + actionSteps: [ + "Offer mentorship to junior colleagues.", + "Lead by example, fostering a culture of collaboration and growth.", + ], + }, + { + level: "", + title: "Master the Artistry of Closing Deals", + video: "https://www.youtube.com/embed/TGbUpEJ1z-k?si=22L4QsXh0awsLbrX", + importance: "Closing high-stakes deals requires finesse and expertise.", + actionSteps: [ + "Study successful deal closures, learning from both successes and challenges.", + "Continuously refine your approach, incorporating insights from experienced peers.", + ], + }, + ] + const [active, setActive] = useState("") + const [top, setTop] = useState(false) + const handleScroll = () => { + if (window.scrollY > 250) { + setTop(true); + } else { + setTop(false); + } + }; + + useEffect(() => { + window.addEventListener("scroll", handleScroll); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); + }; + return ( +
+
+
+
+
+ {salesSkillsArray.map((item, index) => { + return ( + + ) + })} +
+
+ {/* //============================left sidebar */} +
+
    + {salesSkillsArray.map((item, index) => ( +
    +
  • + {item.level} +
  • +
  • + + + {item.title} + +
  • +
    + ))} +
+
+
+
+ {top && ( + + )} +
+ ) +} diff --git a/app/upload/page.tsx b/app/upload/page.tsx new file mode 100644 index 0000000..68048a4 --- /dev/null +++ b/app/upload/page.tsx @@ -0,0 +1,44 @@ +"use client" + +import { useState } from "react" + +export default function ImageUpload() { + const [image, setImage] = useState(null) + + const handleImageChange = (e) => { + const file = e.target.files[0] + setImage(file) + } + + const handleImageUpload = async () => { + const formData = new FormData() + formData.append("file", image) + formData.append("upload_preset", "p2y46g7e") + + try { + const response = await fetch( + `https://api.cloudinary.com/v1_1/drzedrk1e/image/upload`, + { + method: "POST", + body: formData, + } + ) + + if (response.ok) { + const data = await response.json() + console.log("Image uploaded successfully:", data) + } else { + console.error("Error uploading image:", response.statusText) + } + } catch (error) { + console.error("Error uploading image:", error) + } + } + + return ( +
+ + +
+ ) +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..1e18b35 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/components/.DS_Store b/components/.DS_Store new file mode 100644 index 0000000..f0f0bfb Binary files /dev/null and b/components/.DS_Store differ diff --git a/components/AddInvoice.tsx b/components/AddInvoice.tsx new file mode 100644 index 0000000..39431ab --- /dev/null +++ b/components/AddInvoice.tsx @@ -0,0 +1,169 @@ +import React, { useState } from "react" +import Link from "next/link" +import { useRouter } from "next/navigation" +import { format } from "date-fns" +import { + ArrowUpDown, + BookOpenCheck, + Calendar as CalendarIcon, + ChevronDown, + ExternalLink, + FolderEdit, + MoreVertical, + Plus, + Trash2, +} from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Calendar } from "@/components/ui/calendar" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +export default function AddInvoice(props) { + const router = useRouter() + const projectId = props.projectId + const rate = props.rate + const userId = props.user + const upsellerId = props.upsellerId + const { toast } = useToast() + const [isLoading, setLoading] = useState(false) + let [invoiceDate, setinvoiceDate] = useState("") + const [amount, setAmount] = useState("") +console.log(upsellerId) + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + setLoading(true) + + function formatDate(dateString) { + const options = { year: "numeric", month: "long", day: "numeric" } + return new Intl.DateTimeFormat("en-US", options).format( + new Date(dateString) + ) + } + + if (invoiceDate) { + invoiceDate = formatDate(invoiceDate) + } + + if (!invoiceDate || !amount) { + console.log("all fields required") + toast({ + variant: "destructive", + title: "all fields required", + }) + setLoading(false) + return + } + + try { + const res = await fetch("../api/invoice", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + projectId, + userId, + upsellerId, + invoiceDate, + amount, + rate, + }), + }) + if (res.ok) { + toast({ + title: "invoice submited", + }) + setLoading(false) + window.location.reload(true) + } else { + console.log("invoice submit failed!") + toast({ + variant: "destructive", + title: "invoice submit failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during invoice submit:", error) + toast({ + variant: "destructive", + title: `Error during invoice submit:", ${error}`, + }) + setLoading(false) + } + } + + return ( + <> +
+
+
+ + + + + + + + +
+
+ setAmount(e.target.value)} + /> +
+
+ +
+
+
+ + ) +} diff --git a/components/AddSalesrep.tsx b/components/AddSalesrep.tsx new file mode 100644 index 0000000..dd9d863 --- /dev/null +++ b/components/AddSalesrep.tsx @@ -0,0 +1,177 @@ +"use client" + +import React, { FormEvent, useState } from "react" +import { useRouter } from "next/navigation" +import { Plus } from "lucide-react" + +import { sendInvitation } from "@/lib/notification/sendInvitation" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Slider } from "@/components/ui/slider" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +import { Button } from "./ui/button" +import { Input } from "./ui/input" +import { Label } from "./ui/label" + +export default function AddSalesrep(props) { + const { toast } = useToast() + const [name, setName] = useState("") + const [email, setEmail] = useState("") + const [isLoading, setLoading] = useState(false) + + const upSeller = props.upSeller + const upSellerId = props.upSellerId + + const [yourPercentage, setyourPercentage] = useState(8) + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + + setLoading(true) + if (!name || !email) { + console.log("all filled required") + toast({ + variant: "destructive", + title: "all filled required!", + }) + setLoading(false) + return + } + + try { + const resUserExists = await fetch("/api/userExists", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + }) + + const { user } = await resUserExists.json() + + if (user) { + toast({ + variant: "destructive", + title: "User already Exist!", + }) + console.log("User already Exist!") + setLoading(false) + return + } + + const res = await fetch("api/invite", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + email, + upSeller, + upSellerId, + upsellerPercentage: yourPercentage, + }), + }) + + if (res.ok) { + sendInvitation(name, email, upSeller) + toast({ + title: "Invitation Send", + }) + setLoading(false) + window.location.reload() + } else { + console.log("user reg failed!") + toast({ + variant: "destructive", + title: "user reg failed!", + }) + setLoading(false) + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error during reg:" + error, + }) + console.log("Error during reg:", error) + setLoading(false) + } + } + + return ( + + + + + + + +

Add new sales rep under you

+
+ +
+
+
+ + setName(e.target.value)} + placeholder="name" + type="text" + autoCapitalize="none" + autoComplete="name" + autoCorrect="off" + /> +
+
+ + setEmail(e.target.value)} + placeholder="email@example.com" + type="email" + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" + /> +
+
+ setyourPercentage(v[0])} + /> +

+ Your Percentage: {yourPercentage}% +

+
+ +
+
+
+
+
+
+ ) +} diff --git a/components/Clientthumb.tsx b/components/Clientthumb.tsx new file mode 100644 index 0000000..24b2a85 --- /dev/null +++ b/components/Clientthumb.tsx @@ -0,0 +1,31 @@ +import React from "react" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" + +export default function Clientthumb() { + return ( +
+
+ + + + + + CN + +
+

+ Christan Bale +

+
+ ) +} diff --git a/components/Commission.tsx b/components/Commission.tsx new file mode 100644 index 0000000..339481f --- /dev/null +++ b/components/Commission.tsx @@ -0,0 +1,18 @@ +import React, { useEffect, useState } from "react" + +export default function Earnings(props) { + const projectId = props.id + const [data, setData] = useState() + + useEffect(() => { + fetch(`/api/commission?id=${projectId}`) + .then((res) => res.json()) + .then((data) => { + setData(data.commission) + }) + .catch((error) => { + console.error("Error:", error) + }) + }, [projectId]) + return
{data && "$" + data}
+} diff --git a/components/CommissionperProject.tsx b/components/CommissionperProject.tsx new file mode 100644 index 0000000..c23cd21 --- /dev/null +++ b/components/CommissionperProject.tsx @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from "react" + +import { InvoicesbyProjectId } from "@/lib/fetchInvoices" +import { fetchProjectById } from "@/lib/fetchProjects" +import { getUser } from "@/lib/getUser" + +import { Progress } from "./ui/progress" + +export default function CommissionperProject(props) { + const projectId = props.projectId + const [finalAmount, setfinalAmount] = useState() + const [rate, setRate] = useState(0) + const [salesId, setsalesId] = useState() + const [earning, setEarning] = useState(0) + const [contracts, setcontracts] = useState() + const [company, setCompany] = useState() + function findCompanyByName(companyArray, companyName) { + // Using Array.find to find the first object with the matching companyName + return companyArray.find((company) => company.companyName === companyName) + } + + useEffect(() => { + fetchProjectById(projectId).then((data) => { + setsalesId(data?.salesId) + + setCompany(data?.companyName) + }) + + if (salesId) { + getUser(salesId).then((data) => { + setcontracts(data?.contracts) + }) + } + + if (contracts) { + const scontract = findCompanyByName(contracts, company) + if(scontract){ + setRate(scontract?.rate) + } + } + + if (rate) { + InvoicesbyProjectId(projectId).then((data) => { + const paidInvoices = data?.filter( + (invoice) => + invoice.status === "Paid" && invoice.commission_paid === "Yes" + ) + + // Calculate the total amount of paid invoices + const totalAmount = paidInvoices.reduce( + (total, invoice) => total + (invoice.amount / 100) * rate, + 0 + ) + setfinalAmount(totalAmount) + const totalPaid = paidInvoices.reduce( + (total, invoice) => total + invoice.amount, + 0 + ) + setEarning(totalPaid) + }) + } + }, [salesId, contracts]) + + return ( +
+
+ {finalAmount && "$" + finalAmount} +
+ {finalAmount != 0 && earning != 0 && ( + + )} + + {finalAmount == 0 && earning == 0 && ( + + )} +
${earning}
+
+ ) +} diff --git a/components/CompanyLogo.tsx b/components/CompanyLogo.tsx new file mode 100644 index 0000000..2c4a991 --- /dev/null +++ b/components/CompanyLogo.tsx @@ -0,0 +1,41 @@ +import React from "react" +import Image from "next/image" + +export default function CompanyLogo(props) { + const companyName = props.value + let company + + switch (companyName) { + case "WordSphere": + company = "/wordsphere.png" + break + case "moglixy media": + company = "/moglixy.png" + break + case "image appeal": + company = "/Image_appeal.png" + break + case "pixel voyage": + company = "/pixel_voyage.jpg" + break + case "SymbolSense": + company = "/pixel_voyage.jpg" + break + case "zordel": + company = "/zordel.png" + break + case "may levy": + company = "/maylevy.png.jpg" + break + case "cyberly": + company = "/cyberly.png" + break + } + + // {if (companyName =='wordsphere') && } + return ( +
+ company logo +
+ ) +} diff --git a/components/ContractTable.jsx b/components/ContractTable.jsx new file mode 100644 index 0000000..aa1273b --- /dev/null +++ b/components/ContractTable.jsx @@ -0,0 +1,148 @@ +"use client" + +import { useEffect, useState } from "react" +import Link from "next/link" +import { FileEdit, Pencil, Search, Trash, User } from "lucide-react" + +import { getallUser } from "@/lib/getUser" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import AddContract from "../app/settings/manage-contracts/AddContract.tsx" +import DeleteContract from "../app/settings/manage-contracts/DeleteContract" +import EditContract from "../app/settings/manage-contracts/EditContract" +import { Button } from "@/components/ui/button" +const ContractTable = () => { + const [currentPage, setCurrentPage] = useState(1) + const [itemsPerPage] = useState(10) + const [allUser, setAllUser] = useState([]) + const [search, setSearch] = useState("") + const fetchContact = async () => { + const user = await getallUser() + setAllUser(user) + } + + useEffect(() => { + fetchContact() + }, []) + + const indexOfLastItem = currentPage * itemsPerPage + const indexOfFirstItem = indexOfLastItem - itemsPerPage + const currentItems = allUser + ?.filter((item) => { + const searchLowerCase = search.toLowerCase().trim() + if (searchLowerCase === "") { + return true // Return true for all items if search is empty + } else { + return item.name.toLowerCase().includes(searchLowerCase) + } + }) + .slice(indexOfFirstItem, indexOfLastItem) + + const paginate = (pageNumber) => setCurrentPage(pageNumber) + + return ( + <> +
+

Manage Contracts

+
+ + setSearch(event.target.value)} + className="w-full border-0 rounded-none" + /> +
+
+ + + + Name + Contracts + Actions + + + + {currentItems?.length > 0 && + currentItems?.map( + (singleUser) => + singleUser.name !== "super admin" && ( + + +
+ {" "} + + {singleUser.name} + +
+
+ + {singleUser.contracts.map((contract, index) => ( +
+ + {contract.companyName} | {contract.rate}% + +
+ | + +
+
+ ))} +
+ + + +
+ ) + )} +
+
+ {currentItems?.length > 0 && ( +
+ + +
+ )} + + ) +} + +export default ContractTable diff --git a/components/CropImage.tsx b/components/CropImage.tsx new file mode 100644 index 0000000..4df0343 --- /dev/null +++ b/components/CropImage.tsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react' +import ReactCrop from "react-image-crop" +import 'react-image-crop/dist/ReactCrop.css' + +const CropImage = ({file,setResult}) => { + const [image, setImage] = useState(null) + const [crop, setCrop] = useState({ aspect: 1, unit: "%", width: 100, height: 100, x: 0, y: 0 }) + const [croppedImage, setCroppedImage] = useState(null) + const handleImageLoaded = (e) => { + const img = e.target; + setImage(img); + } + + const handleCropChange = (newCrop) => { + console.log(newCrop) + setCrop({ + ...crop, + ...newCrop, + height: newCrop.width * (1 / crop.aspect) + }); + } + + + + const handleCropComplete = (crop) => { + console.log(image) + if (image && crop.width && crop.height) { + getCroppedImg(image, crop, 'profile.jpeg') + } + } + + const getCroppedImg = (image, crop, fileName) => { + if (image && crop) { + const canvas = document.createElement('canvas'); + const scaleX = image.naturalWidth / image.width; + const scaleY = image.naturalHeight / image.height; + + canvas.width = crop.width; + canvas.height = crop.height; + const ctx = canvas.getContext('2d'); + + ctx.drawImage( + image, + crop.x * scaleX, + crop.y * scaleY, + crop.width * scaleX, + crop.height * scaleY, + 0, + 0, + crop.width, + crop.height + ); + + canvas.toBlob((blob) => { + if (blob) { + + const format = 'image/png'; + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => { + setResult(reader.result); + }; + } + }, 'image/png'); + } + } + + const handleCropButtonClick = () => { + getCroppedImg() + } + + return (
+ + {file && + + } +
); +}; + +export default CropImage; diff --git a/components/Earnings.tsx b/components/Earnings.tsx new file mode 100644 index 0000000..24c01cf --- /dev/null +++ b/components/Earnings.tsx @@ -0,0 +1,18 @@ +import React, { useEffect, useState } from "react" + +export default function Earnings(props) { + const projectId = props.id + const [data, setData] = useState() + useEffect(() => { + fetch(`/api/earnings?id=${projectId}`) + .then((res) => res.json()) + .then((data) => { + setData(data.totalEarnings) + }) + .catch((error) => { + console.error("Error:", error) + }) + }, [projectId]) + + return
{data && "$" + data}
+} diff --git a/components/EditProject.tsx b/components/EditProject.tsx new file mode 100644 index 0000000..acd3153 --- /dev/null +++ b/components/EditProject.tsx @@ -0,0 +1,385 @@ +import React, { useState } from "react" +import { useRouter, useSearchParams } from "next/navigation" +import { DropdownMenuCheckboxItemProps } from "@radix-ui/react-dropdown-menu" +import { format } from "date-fns" +import { Calendar as CalendarIcon } from "lucide-react" +import { formatScope,BackToMain } from "@/lib/CreateScope" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Calendar } from "@/components/ui/calendar" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +import Statusbadge from "./statusBadge" + +export default function EditProject(props) { + const data = props.data + + const { toast } = useToast() + const router = useRouter() + + const [status, setStatus] = useState(data.status) + const [isLoading, setLoading] = useState(false) + const [projectName, setprojectName] = useState(data.projectName) + const [salesPerson, setsalesPerson] = useState(data.salesPerson) + const [projectDetails, setprojectDetails] = useState(data.projectDetails) + + const [budget, setbudget] = useState(data.budget) + const [commisson_rate, setcommisson_rate] = useState(data.commisson_rate) + let [dateSigned, setdateSigned] = useState("") + const [clientName, setclientName] = useState(data.clientName) + const [email, setemail] = useState(data.email) + const [phone, setphone] = useState(data.phone) + const [address, setaddress] = useState(data.address) + const [companyName, setcompanyName] = useState(data.companyName) + console.log(address) + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + setLoading(true) + function formatDate(dateString) { + const options = { year: "numeric", month: "long", day: "numeric" } + return new Intl.DateTimeFormat("en-US", options).format( + new Date(dateString) + ) + } + + if (dateSigned) { + dateSigned = formatDate(dateSigned) + } else { + dateSigned = data.dataSigned + } + + try { + const res = await fetch(`/api/project/?id=${data._id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + projectName, + salesPerson, + projectDetails, + companyName, + budget, + status, + commisson_rate, + dateSigned, + clientName, + email, + phone, + address, + }), + }) + + if (res.ok) { + setLoading(false) + toast({ + variant: "default", + title: "Post Updated!", + }) + window.location.reload() + } else { + console.log("project submit failed!") + toast({ + title: "project submit failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during project submit:", error) + toast({ + variant: "destructive", + title: `Error during project submit:", ${error}`, + }) + setLoading(false) + } + } + + return ( +
+
+
+
+ setprojectName(e.target.value.trim())} + defaultValue={data.projectName} + /> +
+
+
+
+
+
+

+ Sales Person: +

+
+
+ setsalesPerson(e.target.value)} + defaultValue={data.salesPerson} + /> +
+
+
+
+

+ Company Name: +

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

+ Status: +

+
+
+ + + + + + + + On Going + + + On Hold + + + Pending + + + Complete + + + + +
+
+ +
+
+

+ Date Signed: +

+
+
+

+ + + + + + + + +

+
+
+ +
+
+

+ Budget: +

+
+
+

+ $ + setbudget(e.target.value.trim())} + defaultValue={data.budget} + /> +

+
+
+ + {/*
+
+

+ Commission Rate: +

+
+
+

+ % + setcommisson_rate(e.target.value.trim())} + defaultValue={data.commisson_rate} + /> +

+
+
*/} +
+
+
+
+

+ Client Name: +

+
+
+

+ setclientName(e.target.value.trim())} + defaultValue={data.clientName} + /> +

+
+
+ +
+
+

+ Email Address: +

+
+
+

+ setemail(e.target.value.trim())} + defaultValue={data.email} + /> +

+
+
+ +
+
+

+ Phone Number: +

+
+
+

+ setphone(e.target.value.trim())} + defaultValue={data.phone} + /> +

+
+
+ +
+
+

+ Client Address: +

+
+
+

+ setaddress(formatScope(e.target.value.trim()))} + defaultValue={BackToMain(data.address)} + /> +

+
+
+
+
+
+

+ Project Details: +

+

+ +

+ +
+
+
+ ) +} diff --git a/components/Editinvoice.tsx b/components/Editinvoice.tsx new file mode 100644 index 0000000..38f7a7b --- /dev/null +++ b/components/Editinvoice.tsx @@ -0,0 +1,234 @@ +"use client" + +import React, { FormEvent, useEffect, useState } from "react" +import { useRouter } from "next/navigation" +import { format } from "date-fns" +import { + ArrowUpDown, + Calendar as CalendarIcon, + ChevronDown, + ExternalLink, + FolderEdit, + MoreVertical, + Plus, + Trash2, +} from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Calendar } from "@/components/ui/calendar" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +import Statusbadge from "./statusBadge" + +export default function Editinvoice(props) { + const { toast } = useToast() + const router = useRouter() + const id = props.value + + const [defaultData, setdefaultdata] = useState() + let [date, setdate] = useState("") + const [status, setStatus] = useState() + const [commissionPaid, setcommissionPaid] = useState() + const [isLoading, setLoading] = useState(false) + + // const [newDate, setNewdate] = useState() + const [newAmount, setNewamount] = useState() + // const [newStatus, setNewstatus] = useState() + + useEffect(() => { + fetchDataFromAPI() + .then((apiData) => { + setdefaultdata(apiData.invoices) + setcommissionPaid(apiData.invoices.commission_paid) + setStatus(apiData.invoices.status) + setNewamount(apiData.invoices.amount) + }) + .catch((error) => { + console.error("Error fetching data:", error) + }) + }, []) + + const fetchDataFromAPI = async () => { + const response = await fetch(`/api/invoice?invoiceid=${id}`) + const data = await response.json() + return data + } + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + setLoading(true) + function formatDate(dateString) { + const options = { year: "numeric", month: "long", day: "numeric" } + return new Intl.DateTimeFormat("en-US", options).format( + new Date(dateString) + ) + } + + if (date) { + date = formatDate(date) + } else { + date = defaultData.invoiceDate + } + + try { + const res = await fetch(`/api/invoice?invoiceid=${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + invoiceDate: date, + amount: newAmount, + status, + commission_paid: commissionPaid, + }), + }) + + if (res.ok) { + setLoading(false) + toast({ + variant: "default", + title: "Invoice Updated!", + }) + window.location.reload() + } else { + console.log("Invoice submit failed!") + toast({ + title: "Invoice submit failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during Invoice submit:", error) + toast({ + variant: "destructive", + title: `Error during Invoice submit:", ${error}`, + }) + setLoading(false) + } + + console.log(date, status, newAmount, commissionPaid) + } + + if (!defaultData) { + return "loading..." + } + + return ( +
+
+
+ +
+ + + + + + + + +
+
+
+ + setNewamount(e.target.value.trim())} + className="col-span-3" + /> +
+
+ + + + + + + + Paid + + Unpaid + + + + +
+
+ + + + + + + + Yes + No + + + +
+ +
+
+ ) +} diff --git a/components/Gettotal.tsx b/components/Gettotal.tsx new file mode 100644 index 0000000..f2806c5 --- /dev/null +++ b/components/Gettotal.tsx @@ -0,0 +1,16 @@ +import React from "react" + +export default function Gettotal(props) { + const invoices = props.data + const filter = props.filter + // Filter invoices with a "Paid" status + const paidInvoices = invoices.filter((invoice) => invoice.status === filter) + + // Calculate the total amount of paid invoices + const totalAmount = paidInvoices.reduce( + (total, invoice) => total + invoice.amount, + 0 + ) + + return <>${totalAmount} +} diff --git a/components/Gettotalcommission.tsx b/components/Gettotalcommission.tsx new file mode 100644 index 0000000..4db9040 --- /dev/null +++ b/components/Gettotalcommission.tsx @@ -0,0 +1,19 @@ +import React from "react" + +export default function Gettotalcommission(props) { + const invoices = props.data + // const filter = props.filter + const rate = props.rate + // Filter invoices with a "Paid" status + const paidInvoices = invoices.filter( + (invoice) => invoice.status === "Paid" && invoice.commission_paid === "Yes" + ) + + // Calculate the total amount of paid invoices + const totalAmount = paidInvoices.reduce( + (total, invoice) => total + (invoice.amount / 100) * rate, + 0 + ) + + return <>${totalAmount} +} diff --git a/components/GrandfatherBadge.tsx b/components/GrandfatherBadge.tsx new file mode 100644 index 0000000..b38e43d --- /dev/null +++ b/components/GrandfatherBadge.tsx @@ -0,0 +1,28 @@ +import React, { useEffect, useState } from "react" + +import { Badge } from "@/components/ui/badge" + +export default function StatsBadge(props) { + const id = props.id + const upSellerId = props.upSellerId + const [commission, setcommission] = useState(0) + + useEffect(() => { + fetch(`/api/commission?upsale=${upSellerId}&sellerId=${id}`) + .then((res) => res.json()) + .then((data) => { + setcommission(data.commission) + }) + .catch((error) => { + console.error("Error:", error) + }) + }, [id,upSellerId]) + return ( +
+ + Commission: {commission ? commission?.toFixed(2) :0} + + +
+ ) +} diff --git a/components/Header.tsx b/components/Header.tsx new file mode 100644 index 0000000..bd73fff --- /dev/null +++ b/components/Header.tsx @@ -0,0 +1,97 @@ +"use client" + +import React, { useEffect, useState } from "react" +import Image from "next/image" +import Link from "next/link" +import { usePathname } from "next/navigation" +import { useSession } from "next-auth/react" + +import Toolkitmenu from "./toolkitMenu" +import UserNav from "./userNav" + +export default function Header() { + const { data: session } = useSession() + const role = session?.user?.role + + const pathname = usePathname() + const [isScrolling, setIsScrolling] = useState(false) + + useEffect(() => { + const handleScroll = () => { + if (!isScrolling && window.scrollY > 0) { + setIsScrolling(true) + } else if (window.scrollY === 0) { + setIsScrolling(false) + } else if (window.scrollY < 250) { + setIsScrolling(false) + } + } + + window.addEventListener("scroll", handleScroll) + + return () => { + window.removeEventListener("scroll", handleScroll) + } + }, []) + // z-[99999999] border-b sticky pb-0 pt-1 transition-all duration-500 ease-in-out top-0bg-white bg-opacity-90 backdrop-blur-sm ${isScrolling?" ":""} + return ( +
+
+
+ + Logo + +
+
+ {(role != "Sales1" || role != "Sales2") && ( +
    +
  • + + Dashboard + +
  • +
  • + + Contracts + +
  • +
  • + + Training + +
  • + {/*
  • + + Sales Meeting + +
  • */} +
+ )} + + {role == "SalesTwo" && } + +
+
+
+ ) +} diff --git a/components/InvoiceTable.tsx b/components/InvoiceTable.tsx new file mode 100644 index 0000000..bc73e7a --- /dev/null +++ b/components/InvoiceTable.tsx @@ -0,0 +1,396 @@ +"use client" + +import React, { useState } from "react" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + ArrowUpDown, + Calendar as CalendarIcon, + ChevronDown, + ExternalLink, + FolderEdit, + MoreVertical, + Plus, + Trash2, +} from "lucide-react" + +import { cn } from "@/lib/utils" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { ToastAction } from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +import Editinvoice from "./Editinvoice" +import Statusbadge from "./statusBadge" + +export default function InvoiceTable(props) { + const data = props.invoiceData + const columns = [ + { + accessorKey: "invoiceDate", + header: ({ column }) => { + return ( +
column.toggleSorting(column.getIsSorted() === "asc")} + > + Date + + + + +
+ ) + }, + cell: ({ row }) => ( +
+ {row.getValue("invoiceDate")} +
+ ), + }, + { + accessorKey: "amount", + header: () =>
Amount
, + cell: ({ row }) => { + const amount = parseFloat(row.getValue("amount")) + + // Format the amount as a dollar amount + const formatted = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount) + + return
{formatted}
+ }, + }, + { + accessorKey: "amount", + header: () =>
Commission
, + cell: ({ row }) => { + const amount = (rate / 100) * row.getValue("amount") + + // Format the amount as a dollar amount + // const formatted = new Intl.NumberFormat("en-US", { + // style: "currency", + // currency: "USD", + // }).format(amount) + + return
${amount?.toFixed(2)}
+ }, + }, + { + accessorKey: "status", + header: ({ column }) => { + return ( +
column.toggleSorting(column.getIsSorted() === "asc")} + > + Status + + + + +
+ ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + accessorKey: "commission_paid", + header: ({ column }) => { + return ( +
column.toggleSorting(column.getIsSorted() === "asc")} + > + Commission Paid + + + + +
+ ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + accessorKey: "actions", + header:
Actions
, + cell: ({ row, column }) => ( +
+ + + + + + + Edit Invoice + + Make changes to your invoice here. Click save when you are + done. + + + + + + + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete + invoice and remove data from servers. + + + + Cancel + { + const res = await fetch( + `/api/invoice?invoiceid=${row.getValue("_id")}`, + { + method: "DELETE", + } + ) + if (res.ok) { + toast({ + variant: "destructive", + title: "Invoice Deleted!", + }) + window.location.reload() + } else { + toast({ + variant: "destructive", + title: "Something went wrong!", + }) + } + }} + > + Delete + + + + +
+ ), + }, + { + accessorKey: "_id", + header: () =>
Id
, + cell: ({ row }) => { + return ( +
+ {row.getValue("_id")} +
+ ) + }, + }, + ] + + const { toast } = useToast() + + const rate = props.rate + + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + if (!table) { + return ( + <> +

loading

+ + ) + } else { + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ {/*
+
+ + +
+
*/} +
+ ) + } +} diff --git a/components/Lists.tsx b/components/Lists.tsx new file mode 100644 index 0000000..2798177 --- /dev/null +++ b/components/Lists.tsx @@ -0,0 +1,117 @@ +"use client" +import React from "react" +import {fetchProjects} from "@/lib/fetchProjects" +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import { Badge } from "./ui/badge" + + + +export default async function Lists() { +const [projects,setProjects]=useSate([]) +const getProjects = async () => { + fetchProjects().then((res)=>{ + setProjects(res.data) + }) +} + useEffect(()=>{ + getProjects() + },[]) + + return ( +
+
+ + + + Project Name + Client Name + +
+ Date Signed + + + + +
+
+ +
+ Contact Amount + + + + +
+
+ Commission + Project Status +
+
+ + {projects.map((t) => ( + + + {t.projectName} + + + {t.clientName} + + {t.dateSigned} + +
+ ${t.budget} +
+
+ $1596 + + + On Going + + +
+ ))} +
+
+
+
+ ) +} diff --git a/components/Loginform.tsx b/components/Loginform.tsx new file mode 100644 index 0000000..882d772 --- /dev/null +++ b/components/Loginform.tsx @@ -0,0 +1,96 @@ +"use client" + +import * as React from "react" +import { useState } from "react" +import { FormEvent } from "react" +import { useRouter } from "next/navigation" +import { signIn } from "next-auth/react" + +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +import { Button } from "./ui/button" +import { Input } from "./ui/input" +import { Label } from "./ui/label" + +export function Loginform() { + const { toast } = useToast() + + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [isLoading, setLoading] = useState(false) + + const router = useRouter() + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + setLoading(true) + try { + const res = await signIn("credentials", { + email, + password, + redirect: false, + }) + if (res.error) { + console.log("Invalid details!") + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: "There was a problem with your information.", + }) + setLoading(false) + return + } + router.replace("dashboard") + } catch (error) { + setLoading(false) + toast({ + variant: "destructive", + title: "Uh oh! Something went wrong.", + description: "There was a problem with your information.", + }) + console.log(error) + } + } + + return ( +
+
+
+
+ + setEmail(e.target.value)} + placeholder="name@example.com" + type="email" + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" + /> +
+
+ + setPassword(e.target.value)} + type="password" + placeholder="Passowrd" + autoCapitalize="none" + autoComplete="password" + autoCorrect="off" + /> +
+ +
+
+
+ ) +} diff --git a/components/PaginationControls.tsx b/components/PaginationControls.tsx new file mode 100644 index 0000000..9224440 --- /dev/null +++ b/components/PaginationControls.tsx @@ -0,0 +1,50 @@ +"use client" + +import { FC } from "react" +import { useRouter, useSearchParams } from "next/navigation" + +interface PaginationControlsProps { + hasNextPage: boolean + hasPrevPage: boolean +} + +const PaginationControls: FC = ({ + hasNextPage, + hasPrevPage, +}) => { + const router = useRouter() + const searchParams = useSearchParams() + + const page = searchParams.get("page") ?? "1" + const per_page = searchParams.get("per_page") ?? "5" + + return ( +
+ + +
+ {page} / {Math.ceil(10 / Number(per_page))} +
+ + +
+ ) +} + +export default PaginationControls diff --git a/components/ReferralComp.tsx b/components/ReferralComp.tsx new file mode 100644 index 0000000..ca5eee0 --- /dev/null +++ b/components/ReferralComp.tsx @@ -0,0 +1,17 @@ +"use client" + +import React, { useEffect } from "react" +import { useSearchParams } from "next/navigation" +import cookie from "js-cookie" + +export default function ReferralComp() { + const searchParams = useSearchParams() + const referral = searchParams.get("ref") + useEffect(() => { + if (referral) { + cookie.set("ref", referral) + } + }, [referral]) + + return <> +} diff --git a/components/ResetPassword.tsx b/components/ResetPassword.tsx new file mode 100644 index 0000000..ac6aa76 --- /dev/null +++ b/components/ResetPassword.tsx @@ -0,0 +1,102 @@ +"use client" + +import React, { useState } from "react" +import { usePathname, useRouter } from "next/navigation" +import { signOut, useSession } from "next-auth/react" +import {Lock } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +const ResetPassword = () => { + const router = useRouter() + const { toast } = useToast() + const [isLoading, setLoading] = useState(false) + + const { data: session } = useSession() + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const handleSubmit = async (e) => { + e.preventDefault() + + if (!password || !confirmPassword) { + toast({ + variant: "destructive", + title: "All fields are required", + }) + // eslint-disable-next-line security/detect-possible-timing-attacks -- comparación de dos campos del mismo formulario, no de un secreto contra un valor almacenado + } else if (password !== confirmPassword) { + toast({ + variant: "destructive", + title: "confirm password not match", + }) + } else { + setLoading(true) + const res = await fetch("/api/resetPassword", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + password, + email: session?.user?.email, + }), + }) + + if (res.ok) { + setLoading(false) + await signOut({ redirect: true }).then(() => {(window.location.href = "/login")}) + toast({ + variant: "default", + title: "password Updated login please!", + }) + } else { + setLoading(false) + console.log("reset submit failed!") + toast({ + variant: "destructive", + title: "reset submit failed!", + }) + setLoading(false) + } + } + } + return ( +
+
+ +
+

+ Now reset your password +

+
+
+ + setPassword(e.target.value)} + type="password" + className="mt-2" + placeholder="Enter new password" + /> +
+
+ + setConfirmPassword(e.target.value)} + type="password" + className="mt-2" + placeholder="Enter confirm password" + /> +
+ +
+
+ ) +} + +export default ResetPassword diff --git a/components/SalesInvoiceTable.tsx b/components/SalesInvoiceTable.tsx new file mode 100644 index 0000000..5d7adc0 --- /dev/null +++ b/components/SalesInvoiceTable.tsx @@ -0,0 +1,329 @@ +"use client" + +import React, { useState } from "react" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + ArrowUpDown, + Calendar as CalendarIcon, + ChevronDown, + ExternalLink, + FolderEdit, + MoreVertical, + Plus, + Trash2, +} from "lucide-react" + +import { cn } from "@/lib/utils" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { ToastAction } from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +import Editinvoice from "./Editinvoice" +import Statusbadge from "./statusBadge" + +export default function SalesInvoiceTable(props) { + const data = props.invoiceData + const columns = [ + { + accessorKey: "invoiceDate", + header: ({ column }) => { + return ( +
column.toggleSorting(column.getIsSorted() === "asc")} + > + Date + + + + +
+ ) + }, + cell: ({ row }) => ( +
+ {row.getValue("invoiceDate")} +
+ ), + }, + { + accessorKey: "amount", + header: () =>
Amount
, + cell: ({ row }) => { + const amount = parseFloat(row.getValue("amount")) + + // Format the amount as a dollar amount + const formatted = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount) + + return
{formatted}
+ }, + }, + { + accessorKey: "amount", + header: () =>
Commission
, + cell: ({ row }) => { + const amount = (rate / 100) * row.getValue("amount") + + // Format the amount as a dollar amount + // const formatted = new Intl.NumberFormat("en-US", { + // style: "currency", + // currency: "USD", + // }).format(amount) + + return
${amount?.toFixed(2)}
+ }, + }, + { + accessorKey: "status", + header: ({ column }) => { + return ( +
column.toggleSorting(column.getIsSorted() === "asc")} + > + Status + + + + +
+ ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + accessorKey: "commission_paid", + header: ({ column }) => { + return ( +
column.toggleSorting(column.getIsSorted() === "asc")} + > + Commission Paid + + + + +
+ ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + accessorKey: "_id", + header: () =>
Id
, + cell: ({ row }) => { + return ( +
+ {row.getValue("_id")} +
+ ) + }, + }, + ] + + const { toast } = useToast() + + const rate = props.rate + + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + if (!table) { + return ( + <> +

loading

+ + ) + } + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ {/*
+
+ + +
+
*/} +
+ ) +} diff --git a/components/SalesTable.tsx b/components/SalesTable.tsx new file mode 100644 index 0000000..d519c2a --- /dev/null +++ b/components/SalesTable.tsx @@ -0,0 +1,255 @@ +"use client" + +import * as React from "react" +import { useState } from "react" +import Image from "next/image" +import Link from "next/link" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + ArrowUpDown, + ChevronDown, + ExternalLink, + FolderEdit, + MoreVertical, + Plus, + Trash2, +} from "lucide-react" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import GrandfatherBadge from "@/components/GrandfatherBadge" + +import CompanyLogo from "./CompanyLogo" +import StatsBadge from "./StatsBadge" + +// const handleRowClick = (projectName) => { +// window.location.href = `/project/${projectName}` +// } + +const handleSalesRowClick = (salesPerson) => { + window.location.href = `/sales/${salesPerson}` +} + +const columns = [ + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => ( +
handleSalesRowClick(row.getValue("name"))} + > + thumb + + {row.getValue("name")} + +
+ ), + }, + { + accessorKey: "_id", + header: "Statistics", + cell: ({ row }) => ( +
+ +
+ ), + }, + { + accessorKey: "upSellerId", + header: "Your Commission", + cell: ({ row }) => ( +
+ +
+ ), + }, + { + accessorKey: "", + header: "Actions", + cell: ({ row }) => ( +
+
handleSalesRowClick(row.getValue("name"))} + > + Manage +
+
+ ), + }, +] + +export default function SalesTable(props) { + const data = props.sales + const [statData, setstatData] = useState("") + + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + return ( + <> +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+
+ + +
+
+ + ) +} diff --git a/components/Salescards.tsx b/components/Salescards.tsx new file mode 100644 index 0000000..7ebe606 --- /dev/null +++ b/components/Salescards.tsx @@ -0,0 +1,221 @@ +import React from "react" + +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" + +export default function salesCards(props) { + const projects = props.projects + const clientName = props.clientName + const totalEarn = props.totalEarn + const completedproject = props.completedproject + return ( + <> +
+ + +
+ + + + + + + + + + Total Projects +
+
+
+ +
+

+ {String(projects.length).padStart(2, "0")} +

+ + 3.9% + + + + +

This Week

+
+
+
+ + +
+ + + + + + Total Clients +
+
+
+ +
+

+ {String(clientName.length).padStart(2, "0")} +

+
+
+
+ + +
+ + + + + + + + + + + + + Project Completed +
+
+
+ +
+

+ {String(completedproject.length).padStart(2, "0")} +

+
+
+
+ + +
+ + + + + Commissions Earned +
+
+
+ +
+

${totalEarn}

+
+
+
+
+ + ) +} diff --git a/components/SellerList.tsx b/components/SellerList.tsx new file mode 100644 index 0000000..a2768cf --- /dev/null +++ b/components/SellerList.tsx @@ -0,0 +1,171 @@ +"user client" + +import React, { useEffect, useState } from "react" +import Image from "next/image" +import Link from "next/link" +import { Search } from "lucide-react" + +import { fetchProjects } from "@/lib/fetchProjects" + +import UpsellerCommission from "./UpsellerCommission" +import TotalClients from "./superAdmin/TotalClients" +import TotalEarning from "./superAdmin/TotalEarning" +import TotalProject from "./superAdmin/TotalProject" +import TotalSales from "./superAdmin/TotalSales" +import { Button } from "./ui/button" + +export default function SellerList(props) { + const userData = props.users + const [currentPage, setCurrentPage] = useState(1) + const postsPerPage = 8 + const [searchTerm, setSearchTerm] = useState("") + const [grandCommission, setgrandCommission] = useState() + const filteredUsers = userData.filter((post) => + post.name.includes(searchTerm) + ) + const startIndex = (currentPage - 1) * postsPerPage + const endIndex = startIndex + postsPerPage + const usersToDisplay = filteredUsers.slice(startIndex, endIndex) + // Determine whether there are more pages to display + const hasMorePages = endIndex < filteredUsers.length + const [data, setData] = useState(null) + useEffect(() => { + fetchProjects() + .then((apiData) => { + if (apiData) { + const uniqueClientNames = [] + + apiData.forEach((item) => { + if (!uniqueClientNames.includes(item.clientName)) { + uniqueClientNames.push(item.clientName) + } + }) + const uniqueSales = [] + apiData.forEach((item) => { + if (!uniqueSales.includes(item.salesPerson)) { + uniqueSales.push(item.salesPerson) + } + }) + setData(apiData) + } + }) + .catch((error) => { + console.error("Error in component:", error) + }) + }, []) + + return ( + <> +
+
+
+

+ <> + {name && ( +
+ Sales People Under + {decodeURIComponent(name)} +
+ )} + {!name && <>Manage Sales People} + +

+
+
+ + setSearchTerm(e.target.value)} + /> +
+
+
+ {usersToDisplay.map((user, index) => ( +
+
+
+ {user.avatar ? ( + {""} + ) : ( + {""} + )} +
+
+

+ + {user.name} + +

+
{user.email}
+
+
+
+
+

+ {user._id && } +

+ + Sales person + +
+
+

+ {user._id && } +

+ Earnings +
+
+

+ +

+ Commission +
+
+
+ ))} +
+
+ {filteredUsers.length > postsPerPage && ( +
+ + + +
+ )} +
+
+ + ) +} diff --git a/components/SetPassword.tsx b/components/SetPassword.tsx new file mode 100644 index 0000000..d025bfb --- /dev/null +++ b/components/SetPassword.tsx @@ -0,0 +1,130 @@ +"use client" + +import React, { FormEvent, useState } from "react" +import { useRouter } from "next/navigation" +import { Label } from "@radix-ui/react-label" + +import { useToast } from "@/components/ui/use-toast" + +import { Icons } from "./icons" +import { Button } from "./ui/button" +import { Input } from "./ui/input" + +export default function SetPassword(props) { + const { toast } = useToast() + const [loading, setLoading] = useState(false) + const [email, setEmail] = useState() + const [password, setPassword] = useState() + const [verifyPassword, setverifyPassword] = useState() + const router = useRouter() + + const handleSubmit = async (e: FormEvent) => { + setLoading(true) + e.preventDefault() + if (!email || !password || !verifyPassword) { + console.log("all filled required") + toast({ + variant: "destructive", + title: "all fields required.", + }) + setLoading(false) + return + } + + // eslint-disable-next-line security/detect-possible-timing-attacks -- comparación de dos campos del mismo formulario, no de un secreto contra un valor almacenado + if (password != verifyPassword) { + console.log("The password you entered does not match") + toast({ + variant: "destructive", + title: "The password you entered does not match", + }) + setLoading(false) + return + } + + try { + const res = await fetch("api/resetPassword", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + password, + }), + }) + + if (res.ok) { + toast({ + title: "user password changed!", + }) + setLoading(false) + // router.push("/") + } else { + console.log("user reg failed!") + toast({ + variant: "destructive", + title: "user reg failed!", + }) + setLoading(false) + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error during set password" + error, + }) + console.log("Error during set password", error) + setLoading(false) + } + } + + return ( +
+
+
+ + setEmail(e.target.value)} + placeholder="Email Address" + type="email" + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" + /> +
+
+ + setPassword(e.target.value)} + placeholder="Password" + type="password" + autoCapitalize="none" + autoComplete="password" + autoCorrect="off" + /> +
+
+ + setverifyPassword(e.target.value)} + placeholder="Verify Password" + type="password" + autoCapitalize="none" + autoComplete="password" + autoCorrect="off" + /> +
+ +
+
+ ) +} diff --git a/components/Signinform.tsx b/components/Signinform.tsx new file mode 100644 index 0000000..a30331c --- /dev/null +++ b/components/Signinform.tsx @@ -0,0 +1,144 @@ +"use client" + +import * as React from "react" +import { useState } from "react" +import { FormEvent } from "react" +import { useRouter } from "next/navigation" + +import { cn } from "@/lib/utils" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +import { Button } from "./ui/button" +import { Input } from "./ui/input" +import { Label } from "./ui/label" + +export function Signinform() { + const { toast } = useToast() + const [name, setName] = useState("") + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [isLoading, setLoading] = useState(false) + + const router = useRouter() + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + setLoading(true) + if (!name || !email || !password) { + console.log("all fields required") + toast({ + variant: "destructive", + title: "all fields required!", + }) + setLoading(false) + return + } + + try { + const resUserExists = await fetch("/api/userExists", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email }), + }) + + const { user } = await resUserExists.json() + + if (user) { + toast({ + variant: "destructive", + title: "User already Exist!", + }) + console.log("User already Exist!") + setLoading(false) + return + } + + const res = await fetch("api/signin", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + email, + password, + }), + }) + + if (res.ok) { + router.push("/") + } else { + console.log("user reg failed!") + toast({ + variant: "destructive", + title: "user reg failed!", + }) + setLoading(false) + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error during reg:" + error, + }) + console.log("Error during reg:", error) + setLoading(false) + } + } + + return ( +
+
+
+
+ + setName(e.target.value.toLowerCase())} + placeholder="Name" + type="text" + autoCapitalize="none" + autoComplete="name" + autoCorrect="off" + /> +
+
+ + setEmail(e.target.value.toLowerCase())} + placeholder="email@example.com" + type="email" + autoCapitalize="none" + autoComplete="email" + autoCorrect="off" + /> +
+
+ + setPassword(e.target.value)} + placeholder="Password" + type="password" + autoCapitalize="none" + autoComplete="password" + autoCorrect="off" + /> +
+ +
+
+
+ ) +} diff --git a/components/SingleCard.tsx b/components/SingleCard.tsx new file mode 100644 index 0000000..dc9d39a --- /dev/null +++ b/components/SingleCard.tsx @@ -0,0 +1,67 @@ +import React from "react" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" + +export default function SingleCard(props) { + const title = props.title + const value = props.value + return ( + <> + + +
+ + + + + + + + + + {title} +
+
+
+ +
+

{value}

+
+
+
+ + ) +} diff --git a/components/StatCard.tsx b/components/StatCard.tsx new file mode 100644 index 0000000..29f5306 --- /dev/null +++ b/components/StatCard.tsx @@ -0,0 +1,73 @@ +import React from "react" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" + +export default function StatCard(props) { + const title = props.title + const value = props.value + const prefix = props.prefix + const suffix = props.suffix + return ( +
+ + +
+ + + + + + + + + + {title} +
+
+
+ +
+

+ {prefix} + {value} + {suffix} +

+
+
+
+
+ ) +} diff --git a/components/StatsBadge.tsx b/components/StatsBadge.tsx new file mode 100644 index 0000000..61f2c2a --- /dev/null +++ b/components/StatsBadge.tsx @@ -0,0 +1,38 @@ +import React, { useEffect, useState } from "react" + +import { Badge } from "@/components/ui/badge" + +export default function StatsBadge(props) { + const id = props.id + const [projects, setprojects] = useState(0) + const [clients, setclients] = useState(0) + const [earnings, setearnings] = useState(0) + const [commission, setcommission] = useState(0) + + useEffect(() => { + fetch(`/api/stat?id=${id}`) + .then((res) => res.json()) + .then((data) => { + setprojects(data.projects) + setclients(data.clients) + setearnings(data.totalAmount) + setcommission(data.commission) + }) + .catch((error) => { + console.error("Error:", error) + }) + }, []) + return ( +
+ + Projects: {projects} + + + Clients: {clients} + + + Earnings: {commission} + +
+ ) +} diff --git a/components/SuperAdmin.tsx b/components/SuperAdmin.tsx new file mode 100644 index 0000000..20cfb6e --- /dev/null +++ b/components/SuperAdmin.tsx @@ -0,0 +1,111 @@ +"user client" + +import React, { useEffect, useState } from "react" +import Image from "next/image" +import Link from "next/link" +import { Plus } from "lucide-react" + +import { fetchProjects } from "@/lib/fetchProjects" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import Datatable from "@/components/datatable" + +import SellerList from "./SellerList" +import SingleCard from "./SingleCard" +import TotalClients from "./superAdmin/TotalClients" +import TotalEarning from "./superAdmin/TotalEarning" +import TotalProject from "./superAdmin/TotalProject" +import { Button } from "./ui/button" + +export default function SuperAdmin(props) { + const userData = props.users + + const [currentPage, setCurrentPage] = useState(1) + const postsPerPage = 8 // Number of posts to display per page + const [searchTerm, setSearchTerm] = useState("") + + const filteredUsers = userData.filter((post) => + post.name.includes(searchTerm) + ) + + const startIndex = (currentPage - 1) * postsPerPage + const endIndex = startIndex + postsPerPage + + // Determine whether there are more pages to display + const hasMorePages = endIndex < filteredUsers.length + + const [data, setData] = useState(null) + + useEffect(() => { + fetchProjects() + .then((apiData) => { + if (apiData) { + const pendingProjects = apiData.filter( + (item) => item.status == "Pending" + ) + + const completedproject = apiData.filter( + (item) => item.status == "Complete" + ) + + const uniqueClientNames = [] + + apiData.forEach((item) => { + if (!uniqueClientNames.includes(item.clientName)) { + uniqueClientNames.push(item.clientName) + } + }) + + const uniqueSales = [] + apiData.forEach((item) => { + if (!uniqueSales.includes(item.salesPerson)) { + uniqueSales.push(item.salesPerson) + } + }) + + setData(apiData) + } + }) + .catch((error) => { + console.error("Error in component:", error) + }) + }, []) + + return ( + <> +
+ + + + +
+ +
+ +
+ + + Sales Guy + + + Projects + + +
+ + + {userData && } + + +

+ Manage Projects +

+ +
+
+
+ + ) +} diff --git a/components/TabComponent.tsx b/components/TabComponent.tsx new file mode 100644 index 0000000..abcb8ca --- /dev/null +++ b/components/TabComponent.tsx @@ -0,0 +1,51 @@ +import { useState } from "react" + +const TabComponent = ({ tabs }) => { + const [activeTab, setActiveTab] = useState(0) + + const handleTabClick = (index) => { + setActiveTab(index) + } + + return ( +
+
+ {tabs.map((tab, index) => ( +
handleTabClick(index)} + > + {tab.title} +
+ ))} +
+ {/* eslint-disable-next-line security/detect-object-injection -- activeTab solo puede ser un índice generado por el propio .map() sobre tabs, nunca input externo */} +
{tabs[activeTab].content}
+ +
+ ) +} + +export default TabComponent diff --git a/components/UpsellerCommission.tsx b/components/UpsellerCommission.tsx new file mode 100644 index 0000000..e2bee53 --- /dev/null +++ b/components/UpsellerCommission.tsx @@ -0,0 +1,24 @@ +import React, { useEffect, useState } from "react" +import {upSellerPercentage} from "@/lib/fetchInvoices" +export default function UpsellerCommission(props) { + const id = props.id + const [commission, setCommission] = useState(0) + function formatNumber(number) { + if (number >= 1e9) { + return (number / 1e9).toFixed(1) + "B" + } else if (number >= 1e6) { + return (number / 1e6).toFixed(1) + "M" + } else if (number >= 1e3) { + return (number / 1e3).toFixed(1) + "K" + } + return number.toString() + } + useEffect(() => { + upSellerPercentage(id).then((res)=>{ + setCommission(res) + }) + + }, [id]) + + return
{commission?formatNumber(commission?.toFixed(1)):0}
+} diff --git a/components/common-ui/AlertBox.tsx b/components/common-ui/AlertBox.tsx new file mode 100644 index 0000000..1f54ba8 --- /dev/null +++ b/components/common-ui/AlertBox.tsx @@ -0,0 +1,29 @@ +import { useState } from "react" +import { Info, X } from "lucide-react" + +import { Alert, AlertTitle } from "@/components/ui/alert" + +export default function AlertBox(props) { + const [showAlert, setshowAlert] = useState(true) + const pendingProject = props.projects + return ( + <> + {showAlert && pendingProject && ( + + + + You have {pendingProject.length} project pending for approval... +
{ + setshowAlert(false) + }} + > + +
+
+
+ )} + + ) +} diff --git a/components/common-ui/HeaderComp.tsx b/components/common-ui/HeaderComp.tsx new file mode 100644 index 0000000..4b068ae --- /dev/null +++ b/components/common-ui/HeaderComp.tsx @@ -0,0 +1,23 @@ +"use client" + +import React from "react" +import { usePathname } from "next/navigation" + +import Header from "../Header" + +export default function HeaderComp() { + let Headercomp + const pathname = usePathname() + + if ( + pathname !== "/" && + pathname !== "/signup" && + pathname !== "/login" && + pathname != "/invitation" + ) { + Headercomp =
+ } else { + Headercomp = "" + } + return <>{Headercomp} +} diff --git a/components/company/Company.tsx b/components/company/Company.tsx new file mode 100644 index 0000000..41a45c0 --- /dev/null +++ b/components/company/Company.tsx @@ -0,0 +1,189 @@ +"use client" + +import React, { useEffect, useState } from "react" +import Image from "next/image" +import Link from "next/link" +import { + Facebook, + Instagram, + Linkedin, + Mail, + MapPin, + Phone, + Twitter, +} from "lucide-react" + +import { fetchCompany } from "@/lib/company/company" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import CompanySkeleton from "@/components/skeleton/CompanySkeleton" + +const Company = (props) => { + const name = props?.name.split("-").join(" ") + const [data, setData] = useState("") + + const fetchCompanyData = async () => { + const companyData = await fetchCompany(name) + console.log(companyData) + setData(companyData) + } + useEffect(() => { + fetchCompanyData() + }, []) + return ( + <> + {data == "" ? ( + + ) : ( + <> +
+
+
+ company logo +
+
+

+ {data && data.companyName} +

+

+ + + + {data && data.companyAddress} +

+

+ + + + + {data && data.companyEmail} + +

+

+ + + + + {data && data.companyPhone} + +

+
+ + + + + + + + + + + + +
+
+
+ + +
+
+
+
+
+ + + + Overview + + + Training + + + Marketing material + + + +

+ Overview +

+

{data && data.overview}

+
+ +

+ Training +

+
+ +

+ marketing material +

+
+
+
+
+ + )} + + ) +} + +export default Company diff --git a/components/company/SignContract.tsx b/components/company/SignContract.tsx new file mode 100644 index 0000000..0f6978f --- /dev/null +++ b/components/company/SignContract.tsx @@ -0,0 +1,194 @@ +"use client" + +import React, { useState } from "react" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Separator } from "@/components/ui/separator" +import { Switch } from "@/components/ui/switch" + +import { Icons } from "../icons" +import { Button } from "../ui/button" +import { Input } from "../ui/input" +import { Label } from "../ui/label" +import { toast } from "../ui/use-toast" + +export default function SignContract(props) { + const AdminEmail = process.env.NEXT_PUBLIC_ADMIN_EMAIL + const [isLoading, setLoading] = useState(false) + const salesPerson = props.salesPerson + const companyName = props.name + const logo = props.logo + const rate = props.rate + const email = props.email + const overview = props.overview + const [isChecked, setChecked] = useState(false) + const [customRate, setcustomRate] = useState() + const handleCheckboxChange = () => { + setChecked(!isChecked) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + let comRate = null + + if (isChecked) { + comRate = customRate + } else { + comRate = rate + } + + try { + const res = await fetch("api/sendEmail/signRequest", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + from: "Sales Fam ", + to: AdminEmail, + subject: "Sign contract request submitted!", + companyName, + salesPerson, + rate: comRate, + }), + }) + + if (res.ok) { + setLoading(false) + toast({ + title: "request submited sucessfully!", + }) + } else { + console.log("request submit failed!") + toast({ + variant: "destructive", + title: "request submit failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during request submit:", error) + toast({ + variant: "destructive", + title: `Error during request submit:", ${error}`, + }) + setLoading(false) + } + } + + return ( + <> + + +
+ Sign Contract +
+
+ + + +
+
+
+ company logo +
+
+

+ {companyName} +

+
+ Commission: {rate}% +
+
+
+
+

{overview}

+
+
+
+ +

+ {salesPerson} +

+
+
+ +

+ {email} +

+
+
+ + +
+
+
+
+ + +
+
+ + {isChecked && ( +
+ + setcustomRate(e.target.value.trim())} + /> +
+ )} + + + +
+
+
+
+
+
+ + ) +} diff --git a/components/datatable.tsx b/components/datatable.tsx new file mode 100644 index 0000000..0d396d7 --- /dev/null +++ b/components/datatable.tsx @@ -0,0 +1,476 @@ +"use client" +import { ArrowLeft ,ArrowRight } from 'lucide-react'; + + +import React,{useState} from "react" +import Image from "next/image" +import Link from "next/link" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + ArrowUpDown, + ChevronDown, + ExternalLink, + FolderEdit, + MoreVertical, + Plus, + Search, + Trash2, +} from "lucide-react" +import { useSession } from "next-auth/react" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" + +import { ScrollArea } from "@/components/ui/scroll-area" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +import AddSalesrep from "./AddSalesrep" +import CompanyLogo from "./CompanyLogo" +import Earnings from "./Earnings" +import Statusbadge from "./statusBadge" + +// const handleEditClick = (projectName) => { +// window.location.href = `/project/${projectName}/?edit=true` +// } + +const handleRowClick = (projectName) => { + window.location.href = `/project/${encodeURIComponent(projectName)}` +} + +const handleSalesRowClick = (salesPerson) => { + window.location.href = `/sales/${salesPerson}` +} + +const columns: ColumnDef[] = [ + { + accessorKey: "projectName", + header: "Project Name", + cell: ({ row }) => ( +
handleRowClick(row.getValue("projectName"))} + > + {/* {parseInt(row.id) + 1} */} + + + + + + {row.getValue("projectName")} +
+ ), + }, + { + accessorKey: "salesPerson", + header: "Sales Person", + cell: ({ row }) => ( +
handleSalesRowClick(row.getValue("salesPerson"))} + > +
{row.getValue("salesPerson")}
+
+ ), + }, + { + accessorKey: "clientName", + header: "Client Name", + cell: ({ row }) => ( +
{row.getValue("clientName")}
+ ), + }, + { + accessorKey: "_id", + header: ({ column }) => { + return
Earnings
+ }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + accessorKey: "companyName", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + id: "status", + enableHiding: true, + accessorKey: "status", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + accessorKey: "action", + header: "Action", + cell: ({ row }) => ( +
+
    + + + +
  • handleRowClick(row.getValue("projectName"))}> + +
  • +
    + +

    View Details

    +
    +
    +
    + + + +
  • + + + + + + + + Are you absolutely sure? + + + This action cannot be undone. This will permanently + delete the project and remove data from our servers. + + + + Cancel + { + const res = await fetch( + `/api/project?id=${row.original._id}`, + { + method: "DELETE", + } + ) + + const res2 = await fetch( + `/api/invoice?projectid=${row.original._id}`, + { + method: "DELETE", + } + ) + + if (res.ok && res2.ok) { + window.location.reload() + } + }} + > + Remove + + + + +
  • +
    + +

    Delete

    +
    +
    +
    +
+
+ ), + }, +] + +export default function DataTable(props) { + const { data: session } = useSession() + const role = session?.user?.role + const data = props.projects + const Allcompanies = props.Allcompanies + const userName = props.userName + const id = props.id + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + return ( + <> +
+
+ + {!(role === "Admin-IA") || + (role == "SuperAdmin" && ( + + ))} +
+
+ + + table.getColumn("projectName")?.setFilterValue(event.target.value) + } + className="w-full border-0" + /> +
+
+ + {/* */} +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+
+ + + +
+
+ + ) +} diff --git a/components/datatableSales.tsx b/components/datatableSales.tsx new file mode 100644 index 0000000..4dcaf22 --- /dev/null +++ b/components/datatableSales.tsx @@ -0,0 +1,346 @@ +"use client" + +import * as React from "react" +import Link from "next/link" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { Plus,Search } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { ScrollArea } from "@/components/ui/scroll-area" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import Commission from "./Commission" +import CommissionperProject from "./CommissionperProject" +import CompanyLogo from "./CompanyLogo" +import Earnings from "./Earnings" +import Gettotalcommission from "./Gettotalcommission" +import Statusbadge from "./statusBadge" + +const handleRowClick = (projectName) => { + window.location.href = `/project/${projectName}` +} + +const columns: ColumnDef[] = [ + { + accessorKey: "projectName", + header: "Project Name", + cell: ({ row }) => ( +
handleRowClick(row.getValue("projectName"))} + > + {/* {parseInt(row.id) + 1} */} + + + + + + {row.getValue("projectName")} +
+ ), + }, + { + accessorKey: "clientName", + header: "Client Name", + cell: ({ row }) => ( +
{row.getValue("clientName")}
+ ), + }, + { + accessorKey: "_id", + header: ({ column }) => { + return
Earnings
+ }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + accessorKey: "companyName", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + id: "status", + enableHiding: true, + accessorKey: "status", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, +] + +export default function DataTable(props) { + const allinvoice = props.allinvoice + const data = props?.projects||[] + const AllCompanies = props.AllCompanies + + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + return ( + <> +
+
+ + + + +
+ + + table?.getColumn("projectName")?.setFilterValue(event.target.value) + } + className="max-w-sm px-2 border-0" + /> +
+
+
+ +
+
+
+ + + {table?.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table?.getRowModel().rows?.length ? ( + table?.getRowModel().rows.map((row) => ( + <> + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+
+ + +
+
+ + ) +} diff --git a/components/datatableSeller1.tsx b/components/datatableSeller1.tsx new file mode 100644 index 0000000..d6c5d0f --- /dev/null +++ b/components/datatableSeller1.tsx @@ -0,0 +1,382 @@ +"use client" + +import * as React from "react" +import Image from "next/image" +import Link from "next/link" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { + ArrowUpDown, + ChevronDown, + ExternalLink, + FolderEdit, + MoreVertical, + Plus, + Trash2,Search +} from "lucide-react" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { ScrollArea } from "@/components/ui/scroll-area" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +import AddSalesrep from "./AddSalesrep" +import CommissionperProject from "./CommissionperProject" +import CompanyLogo from "./CompanyLogo" +import Earnings from "./Earnings" +import Statusbadge from "./statusBadge" +import { useSession } from "next-auth/react" + +// const handleEditClick = (projectName) => { +// window.location.href = `/project/${projectName}/?edit=true` +// } + +const handleRowClick = (projectName) => { + window.location.href = `/project/${projectName}` +} + +const handleSalesRowClick = (salesPerson) => { + window.location.href = `/sales/${salesPerson}` +} + +const columns: ColumnDef[] = [ + { + accessorKey: "projectName", + header: "Project Name", + cell: ({ row }) => ( +
handleRowClick(row.getValue("projectName"))} + > + {/* {parseInt(row.id) + 1} */} + + + + + + {row.getValue("projectName")} +
+ ), + }, + { + accessorKey: "clientName", + header: "Client Name", + cell: ({ row }) => ( +
{row.getValue("clientName")}
+ ), + }, + { + accessorKey: "_id", + header: ({ column }) => { + return
Earnings
+ }, + cell: ({ row }) => ( +
+
+ +
+
+ ), + }, + { + accessorKey: "companyName", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, + { + id: "status", + enableHiding: true, + accessorKey: "status", + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => ( +
+ +
+ ), + }, +] + +export default function DataTableSeller1(props) { + + const { data: session } = useSession() + const role = session?.user?.role + const data = props.projects + const Allcompanies = props.Allcompanies + const userName = props.userName + const id = props.id + + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + return ( + <> +
+
+ + + +
+
+ + + table.getColumn("projectName")?.setFilterValue(event.target.value) + } + className="w-full px-2 border-0 rounded-none" + /> +
+
+ + +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+
+ + +
+
+ + ) +} diff --git a/components/email-template/DefaultTemplate.tsx b/components/email-template/DefaultTemplate.tsx new file mode 100644 index 0000000..4d9cc49 --- /dev/null +++ b/components/email-template/DefaultTemplate.tsx @@ -0,0 +1,119 @@ +import * as React from "react" +import { + Body, + Container, + Head, + Heading, + Hr, + Html, + Img, + Link, + Preview, + Section, + Text, +} from "@react-email/components" + +interface EmailPlaceholderProps { + name: string + upseller: string + subject: string +} + +const baseUrl = "http://salesfam.com" + +export const DefaultTemplate = ({ + name, + upseller, + subject: subject, +}: DefaultTemplate) => ( + + + Unleash Your Sales Potential with Sales Fam. + + + Sales Fam + {subject} +
+ + Hello, {name}
+ {upseller} would like you to join as a sales rep at SalesFam. + Please click on the accept button to accept the invitation. +
+ + 👉 Accept Invitation 👈 + +
+ SalesFam Team +
+ Sales Fam + Sales Fam + 2010-2024 All Rights Reserved +
+ + +) + +export default DefaultTemplate + +const main = { + backgroundColor: "#ffffff", + fontFamily: + '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', +} + +const container = { + margin: "0 auto", + padding: "20px 25px 48px", + backgroundImage: 'url("/assets/raycast-bg.png")', + backgroundPosition: "bottom", + backgroundRepeat: "no-repeat, no-repeat", +} + +const heading = { + fontSize: "28px", + fontWeight: "bold", + marginTop: "48px", +} + +const body = { + margin: "24px 0", +} + +const paragraph = { + fontSize: "16px", + lineHeight: "26px", +} + +const link = { + color: "#FF6363", +} + +const hr = { + borderColor: "#dddddd", + marginTop: "48px", +} + +const footer = { + color: "#8898aa", + fontSize: "12px", + marginLeft: "4px", +} diff --git a/components/email-template/ResetTemplate.tsx b/components/email-template/ResetTemplate.tsx new file mode 100644 index 0000000..57fcc52 --- /dev/null +++ b/components/email-template/ResetTemplate.tsx @@ -0,0 +1,107 @@ +import * as React from "react" +import { + Body, + Container, + Head, + Html, + Img, + Link, + Section, + Text, +} from "@react-email/components" + +interface PasswordResetRequestProps { + salesName: string + mainEmail: string +} + +const company = "SalesFam" +// const baseUrl = "http://localhost:8080" +const baseUrl = "https://salesfam.com" + +export const PasswordResetRequest: React.FC = ({ + salesName, + mainEmail, +}) => ( + + + + + {company} +
+ Dear {salesName}, + + You recently requested a password reset for your account. Please + follow the instructions below to reset your password: + + + Reset Password + + + If you encounter any issues, feel free to reach out to our support + team at {mainEmail} or by replying to this email. + + Best regards, + Sales Fam + © 2010-2024 {company}. All Rights Reserved +
+
+ + +) + +const main = { + backgroundColor: "#f5f5f5", + fontFamily: "Arial, sans-serif", +} + +const container = { + margin: "0 auto", + padding: "20px 25px 48px", +} + +const body = { + margin: "24px 0", +} + +const heading = { + fontSize: "18px", + fontWeight: "bold", + marginBottom: "16px", +} + +const paragraph = { + fontSize: "16px", + lineHeight: "1.5", + marginBottom: "16px", +} + +const resetPass = { + color: "#007bff", + fontSize: "16px", + textDecoration: "none", +} + +const closing = { + marginTop: "24px", +} + +const signature = { + fontSize: "18px", + fontWeight: "bold", +} + +const footer = { + fontSize: "12px", + color: "#6c757d", + marginTop: "24px", +} diff --git a/components/email-template/email-placeholder.tsx b/components/email-template/email-placeholder.tsx new file mode 100644 index 0000000..4e65509 --- /dev/null +++ b/components/email-template/email-placeholder.tsx @@ -0,0 +1,112 @@ +import * as React from "react" +import { + Body, + Container, + Head, + Heading, + Hr, + Html, + Img, + Link, + Preview, + Section, + Text, +} from "@react-email/components" + +interface EmailPlaceholderProps { + projectName: string + salesPerson: string + subject: string +} + +const baseUrl = "http://salesfam.com" + +export const EmailPlaceholder = ({ + projectName, + salesPerson, + subject: subject, +}: EmailPlaceholderProps) => ( + + + Unleash Your Sales Potential with Sales Fam. + + + Sales Fam + {subject} +
+ + A new project submited by {salesPerson}
+ Project Name: {projectName} +
+ Please review it. +
+ SalesFam Team +
+ Sales Fam + Sales Fam + 2010-2024 All Rights Reserved +
+ + +) + +export default EmailPlaceholder + +const main = { + backgroundColor: "#ffffff", + fontFamily: + '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', +} + +const container = { + margin: "0 auto", + padding: "20px 25px 48px", + backgroundImage: 'url("/assets/raycast-bg.png")', + backgroundPosition: "bottom", + backgroundRepeat: "no-repeat, no-repeat", +} + +const heading = { + fontSize: "28px", + fontWeight: "bold", + marginTop: "48px", +} + +const body = { + margin: "24px 0", +} + +const paragraph = { + fontSize: "16px", + lineHeight: "26px", +} + +const link = { + color: "#FF6363", +} + +const hr = { + borderColor: "#dddddd", + marginTop: "48px", +} + +const footer = { + color: "#8898aa", + fontSize: "12px", + marginLeft: "4px", +} diff --git a/components/email-template/email-send.tsx b/components/email-template/email-send.tsx new file mode 100644 index 0000000..9a59b06 --- /dev/null +++ b/components/email-template/email-send.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { + Body, + Container, + Head, + Heading, + Hr, + Html, + Img, + Link, + Preview, + Section, + Text, +} from "@react-email/components" +import Logo from "public/logo.png" + +interface RaycastMagicLinkEmailProps { + rate: string + salesPerson: string + companyName: string + subject: string +} + +const baseUrl = "http://salesfam.com" + +export const RaycastMagicLinkEmail = ({ + salesPerson: salesPerson, + companyName: companyName, + rate: rate, + subject: subject, +}: RaycastMagicLinkEmailProps) => ( + + + Unleash Your Sales Potential with Sales Fam. + + + Sales Fam + {subject} +
+ + Sales person: {salesPerson}
+ Company Name: {companyName}
+ Commission rate: {rate} +
+
+ SalesFam Team +
+ Sales Fam + Sales Fam + 2010-2024 All Rights Reserved +
+ + +) + +export default RaycastMagicLinkEmail + +const main = { + backgroundColor: "#ffffff", + fontFamily: + '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', +} + +const container = { + margin: "0 auto", + padding: "20px 25px 48px", + backgroundImage: 'url("/assets/raycast-bg.png")', + backgroundPosition: "bottom", + backgroundRepeat: "no-repeat, no-repeat", +} + +const heading = { + fontSize: "28px", + fontWeight: "bold", + marginTop: "48px", +} + +const body = { + margin: "24px 0", +} + +const paragraph = { + fontSize: "16px", + lineHeight: "26px", +} + +const link = { + color: "#FF6363", +} + +const hr = { + borderColor: "#dddddd", + marginTop: "48px", +} + +const footer = { + color: "#8898aa", + fontSize: "12px", + marginLeft: "4px", +} diff --git a/components/icons.tsx b/components/icons.tsx new file mode 100644 index 0000000..8862f8b --- /dev/null +++ b/components/icons.tsx @@ -0,0 +1,47 @@ +import { + LucideProps, + Moon, + SunMedium, + Twitter, + type Icon as LucideIcon, +} from "lucide-react" + +export type Icon = LucideIcon + +export const Icons = { + sun: SunMedium, + moon: Moon, + twitter: Twitter, + logo: (props: LucideProps) => ( + + + + ), + gitHub: (props: LucideProps) => ( + + + + ), + spinner: (props: LucideProps) => ( + + + + ), +} diff --git a/components/landing/Clients.tsx b/components/landing/Clients.tsx new file mode 100644 index 0000000..71283da --- /dev/null +++ b/components/landing/Clients.tsx @@ -0,0 +1,96 @@ +import React from "react" + +export default function Clients() { + return ( +
+
+
+
+

+ Trusted by Countless Businesses Worldwide +

+
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+
+ +
+
+

+ Join a thriving ecosystem where your expertise meets opportunity +

+
+ +
+

+ No need for spreadsheets, manual data entry, and sales chaos +

+
+
+
+ +
+

+ Our cutting-edge software is designed to empower your sales + team and drive revenue growth +

+
+
+
+ +
+

+ We match you with companies offering lucrative commissions for + promoting their products and services +

+
+
+
+
+
+
+ ) +} diff --git a/components/landing/Help.tsx b/components/landing/Help.tsx new file mode 100644 index 0000000..2cf3d00 --- /dev/null +++ b/components/landing/Help.tsx @@ -0,0 +1,157 @@ +"use client" + +import { useRef, useState } from "react" +import { Play } from "lucide-react" + +export default function Help() { + const [isPlaying, setIsPlaying] = useState(false) + const videoRef = useRef(null) + + const togglePlayPause = () => { + const video = videoRef.current + + if (video) { + if (isPlaying) { + video.pause() + } else { + video.play() + } + + setIsPlaying(!isPlaying) + } + } + + return ( +
+
+
+ {/* Video Start */} +
+ {/*
+ +
+
+ +
*/} + +
+ {/* Video End */} + {/* Manage start*/} +
+
+

+ Manage Sales effectively on our dashboard through SaaS +

+

+ The Software as a Service (SaaS) delivery model has become + prevalent across various business applications, encompassing + office software, messaging tools, payroll processing systems, + database management software, and management solutions +

+
+
+
+ + + + + +
+

4.9 / 5 rating

+

databricks

+
+
+
+ + + + + +
+

4.9 / 5 rating

+

databricks

+
+
+
+ {/* Right side */} +
+
+ +
+

+ Publishing +

+

+ Plan, collaborate, and publish your content to foster + meaningful engagement and growth for your brand. A + structured approach to delivering changes to the software + while minimizing disruption to users and maximizing the + value delivered by the updates. +

+
+
+
+ +
+

+ Analytics +

+

+ Analyze your performance; create effective, detailed and + relevant reports. Gain valuable insights into user behavior, + make data-driven decisions to improve the software, and + ultimately enhance the overall user experience and business + performance. +

+
+
+
+ +
+

+ Engagement +

+

+ Creating an environment that encourages active + participation, fosters a sense of community, and delivers + ongoing value to users. +

+
+
+
+
+ {/* Manage End */} +
+
+
+ ) +} diff --git a/components/landing/Hero.tsx b/components/landing/Hero.tsx new file mode 100644 index 0000000..a7fcedf --- /dev/null +++ b/components/landing/Hero.tsx @@ -0,0 +1,50 @@ +import React from "react" +import Link from "next/link" + +export default function Hero() { + return ( +
+
+
+
+

+ Unleash Your Sales Potential with{" "} + + Sales Fam + +

+

+ Welcome to our dynamic sales platform where commission-based + representatives connect with top-tier companies ready to + supercharge their sales efforts +

+
    +
  • + + Get Started + +
  • +
  • + + View Pricing + +
  • +
+
+
+
+
+ +
+
+ ) +} diff --git a/components/landing/Pricing.tsx b/components/landing/Pricing.tsx new file mode 100644 index 0000000..08f3f66 --- /dev/null +++ b/components/landing/Pricing.tsx @@ -0,0 +1,171 @@ +"use client" + +import React, { useState } from "react" + +export default function Pricing() { + const [active, setActive] = useState("monthly") + + const handleClick = (value) => { + setActive(value) + } + + return ( +
+
+
+
+

+ Flexible Pricing +

+

+ Choose the pricing plan that fits your needs +

+

+ Our Plans - Individual, Small team, Growing Enterprise - Tailored + to your goals +

+
+ + +
+
+
+
+

+ Lite +

+

+ For individuals and small teams trying out for an unlimited + period. +

+

+ $ + {active == "monthly" && "29"} + {active == "yearly" && "50"} + + /{active == "monthly" && "month"} + {active == "yearly" && "year"} + +

+ + Get Started + +
    +
  • + Own + terms selling +
  • +
  • + Robust + integrations +
  • +
  • + One + time payment +
  • +
+
+
+
+
+

+ Basic +

+

+ For individual account executives who want increased + productivity. +

+

+ $ + {active == "monthly" && "49"} + {active == "yearly" && "120"} + + /{active == "monthly" && "month"} + {active == "yearly" && "year"} + +

+ + Get Started + +
    +
  • + Live + streaming +
  • +
  • + No + bandwidth +
  • +
  • + {" "} + Marketing tools +
  • +
+
+
+
+
+

+ Plus +

+

+ For medium and large sales organizations with advanced + needs. +

+

+ $ + {active == "monthly" && "99"} + {active == "yearly" && "200"} + + {" "} + /{active == "monthly" && "month"} + {active == "yearly" && "year"} + +

+ + Get Started + +
    +
  • + Own + terms selling +
  • +
  • + Robust + integrations +
  • +
  • + One + time payment +
  • +
+
+
+
+
+
+
+
+ ) +} diff --git a/components/landing/ProfileSlider.tsx b/components/landing/ProfileSlider.tsx new file mode 100644 index 0000000..c74fa56 --- /dev/null +++ b/components/landing/ProfileSlider.tsx @@ -0,0 +1,221 @@ +"use client" + +import React, { useRef, useState } from "react" +// Import Swiper React components +import { Swiper, SwiperSlide } from "swiper/react" + +// Import Swiper styles +import "swiper/css" +import "swiper/css/pagination" +import Link from "next/link" +// import required modules +import { Autoplay, Navigation, Pagination } from "swiper/modules" + +const sliderData = [ + { + name: "Frederic Hill", + avatar: "./slider_profile_1.png", + designation: "Founder & CEO", + content: + "Incredible efficiency boost! Our team's productivity skyrocketed since we started using SalesFam’s SaaS solution. Seamless integration and intuitive features make our workflow a breeze.", + social: { + fb: "https://facebook.com", + tw: "https://twitter.com", + ln: "https://linkedin.com", + }, + }, + { + name: "Emma Smith", + avatar: "./slider_profile_2.png", + designation: "Founder & CEO", + content: + "Game-changer for our business! With this SalesFam SaaS platform, we've streamlined our operations and gained valuable insights. Highly recommend for anyone seeking to scale up effortlessly.", + social: { + fb: "https://facebook.com", + tw: "https://twitter.com", + ln: "https://linkedin.com", + }, + }, + { + name: "Liam Johnson", + avatar: "./slider_profile_3.png", + designation: "Founder & CEO", + content: + "Top-notch support and results! From onboarding to ongoing assistance, the team behind the SalesFam SaaS has been phenomenal. Our ROI has exceeded expectations, thanks to their innovative approach.", + social: { + fb: "https://facebook.com", + tw: "https://twitter.com", + ln: "https://linkedin.com", + }, + }, + { + name: "Olivia Williams", + avatar: "./slider_profile_4.png", + designation: "Founder & CEO", + content: + "Incredible efficiency boost! Our team's productivity skyrocketed since we started using SalesFam’s SaaS solution. Seamless integration and intuitive features make our workflow a breeze.", + social: { + fb: "https://facebook.com", + tw: "https://twitter.com", + ln: "https://linkedin.com", + }, + }, + { + name: "Noah Brown", + avatar: "./slider_profile_1.png", + designation: "Founder & CEO", + content: + "Game-changer for our business! With this SalesFam SaaS platform, we've streamlined our operations and gained valuable insights. Highly recommend for anyone seeking to scale up effortlessly.", + social: { + fb: "https://facebook.com", + tw: "https://twitter.com", + ln: "https://linkedin.com", + }, + }, + { + name: "Ava Jones", + avatar: "./slider_profile_2.png", + designation: "Founder & CEO", + content: + "Top-notch support and results! From onboarding to ongoing assistance, the team behind the SalesFam SaaS has been phenomenal. Our ROI has exceeded expectations, thanks to their innovative approach.", + social: { + fb: "https://facebook.com", + tw: "https://twitter.com", + ln: "https://linkedin.com", + }, + }, + { + name: "William Davis", + avatar: "./slider_profile_3.png", + designation: "Founder & CEO", + content: + "Incredible efficiency boost! Our team's productivity skyrocketed since we started using SalesFam’s SaaS solution. Seamless integration and intuitive features make our workflow a breeze.", + social: { + fb: "https://facebook.com", + tw: "https://twitter.com", + ln: "https://linkedin.com", + }, + }, + { + name: "Sophia Miller", + avatar: "./slider_profile_4.png", + designation: "Founder & CEO", + content: + "Game-changer for our business! With this SalesFam SaaS platform, we've streamlined our operations and gained valuable insights. Highly recommend for anyone seeking to scale up effortlessly.", + social: { + fb: "https://facebook.com", + tw: "https://twitter.com", + ln: "https://linkedin.com", + }, + }, + { + name: "James Wilson", + avatar: "./slider_profile_4.png", + designation: "Founder & CEO", + content: + "Top-notch support and results! From onboarding to ongoing assistance, the team behind the SalesFam SaaS has been phenomenal. Our ROI has exceeded expectations, thanks to their innovative approach.", + social: { + fb: "https://facebook.com", + tw: "https://twitter.com", + ln: "https://linkedin.com", + }, + }, +] + +export default function ProfileSlider() { + return ( + <> +
+
+
+
+

+ Testimonials +

+
+
+

+ We offer a variety of interesting features to help increase your + productivity at work and manage your project effectively and + efficiently +

+
+
+ + Get Started + +
+
+ + + {sliderData.map((singleSlider, index) => { + return ( + +
+
+ +

+ {singleSlider.name} +

+

+ {singleSlider.designation} +

+

{singleSlider.content}

+
    +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
+
+
+
+ ) + })} +
+
+
+ + ) +} diff --git a/components/landing/Software.tsx b/components/landing/Software.tsx new file mode 100644 index 0000000..2b96f49 --- /dev/null +++ b/components/landing/Software.tsx @@ -0,0 +1,121 @@ +import React from "react" +import Link from "next/link" + +export default function Software() { + return ( +
+
+ {/* top start*/} +
+
+

+ Multiple Software
+ Integration - 1 Dashboard +

+

+ Join over 2,000 rapidly expanding brands leveraging Sales Fam to + accelerate growth through centralized data analysis and + dissemination +

+
    +
  • + + Get Started + +
  • +
  • + + View Pricing + +
  • +
+
+ {/* Right side */} +
+ +
+
+ {/* top End */} + {/* mid start */} +
+
+

+ Features +

+
+
+

+ Explore a range of exciting features designed to enhance your + productivity and simplify project management +

+
+
+ + Get Started + +
+
+ {/* mid end */} +
+
+ +

+ Team Collaboration +

+

+ Virtually and collectively manage projects with your team. Enable + organizations to leverage the collective knowledge, skills, and + expertise of team members to achieve shared goals, drive + innovation, and deliver high-quality outcomes +

+
+
+ +

+ Cloud Storage +

+

+ Storage concerns - we offer up to 2 TB Provide a convenient, + scalable, and secure solution for storing, accessing, and sharing + data, enabling individuals and organizations to leverage the + benefits of cloud computing for their storage needs +

+
+
+ +

+ Daily Analytics +

+

+ We consistently deliver valuable insights to simplify your daily + operations. Enable organizations to harness the power of data to + drive continuous improvement, innovation, and competitive + advantage in today‘s fast-paced business environment +

+
+
+
+
+ ) +} diff --git a/components/landing/Success.tsx b/components/landing/Success.tsx new file mode 100644 index 0000000..3c588c7 --- /dev/null +++ b/components/landing/Success.tsx @@ -0,0 +1,57 @@ +import React from "react" + +export default function Success() { + return ( +
+
+
+
+

Success Numbers

+

+ Quantitative Results +

+

+ Our quantifiable outcomes, achievements, and metrics signify the + use of data-driven evidence and statistics to measure the impact + of performance, progress, and success +

+
+
+

+ 90 % +

+

+ Productivity Improvements +

+
+
+

+ 75 % +

+

+ Revenue Growth +

+
+
+

+ 99 % +

+

+ Customer Acquisition Rates +

+
+
+

+ 240 % +

+

+ Cost Savings +

+
+
+
+
+
+
+ ) +} diff --git a/components/landing/common/Footer.tsx b/components/landing/common/Footer.tsx new file mode 100644 index 0000000..103abeb --- /dev/null +++ b/components/landing/common/Footer.tsx @@ -0,0 +1,265 @@ +import React from "react" +import Link from "next/link" + +export default function Footer() { + return ( +
+
+ {/* footer layer one */} +
+
+
+

+ Ready to Transform Your Business? +

+

+ Explore a range of exciting features designed to enhance your + productivity and simplify project management +

+ + Get Started + +
+
+

+ Find us on Social Media +

+
    +
  • +

    Call Us

    + + (209) 890-8565 + +
  • +
  • +

    Email Us

    + + info@salesfam.com + +
  • +
+

+ Find us on Social Media +

+
    +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
+
+
+
+ {/* footer layer two */} +
+
+
+

+ Quick Link +

+
    +
  • + + Home + +
  • +
  • + + About Us + +
  • +
  • + + Features + +
  • +
  • + + Solution + +
  • +
  • + + Pricing + +
  • +
+
+
+

+ Services +

+
    +
  • + + Commerce + +
  • +
  • + + Payments + +
  • +
  • + + Point of sale + +
  • +
  • + + Stock Management + +
  • +
  • + + Customer Directory + +
  • +
+
+
+

+ Resource +

+
    +
  • + + Blog + +
  • +
  • + + Support + +
  • +
  • + + Help Center + +
  • +
  • + + Tutorials + +
  • +
+
+
+

+ Join our newsletter +

+

+ Keep up to date with everything Reflect +

+
+ + +
+
+
+
+ {/* footer layer three */} +
+
+
+
    +
  • + + Privacy Policy + +
  • +
  • +

    .

    +
  • +
  • + + Terms of Conditions + +
  • +
+
+
+

+ © 2010-2024 SalesFam - All Rights Reserved. +

+
+
+
+
+
+ ) +} diff --git a/components/landing/common/Header.tsx b/components/landing/common/Header.tsx new file mode 100644 index 0000000..069ae6a --- /dev/null +++ b/components/landing/common/Header.tsx @@ -0,0 +1,171 @@ +"use client" + +import React, { useEffect, useState } from "react" +import Link from "next/link" +import { Menu } from "lucide-react" + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +export default function Header() { + const [isScrolling, setIsScrolling] = useState(false) + + useEffect(() => { + const handleScroll = () => { + if (!isScrolling && window.scrollY > 250) { + setIsScrolling(true) + } else if (window.scrollY === 0) { + setIsScrolling(false) + } + } + + window.addEventListener("scroll", handleScroll) + + return () => { + window.removeEventListener("scroll", handleScroll) + } + }, []) + return ( +
+
+
+
+
+ + + +
+
+
+
    +
  • + + About + +
  • +
  • + + Pricing + +
  • +
  • + + Solutions + +
  • +
  • + + Blog + +
  • +
  • + + Contact + +
  • +
  • + + Login + +
  • +
+
+ + +
+ +
+
+ +
    +
  • + + Why Sales Fam + +
  • +
  • + + Pricing + +
  • +
  • + + Solutions + +
  • +
  • + + Blog + +
  • +
  • + + Contact Us + +
  • +
  • + + Login + +
  • +
+
+
+
+
+
+
+
+ ) +} diff --git a/components/main-nav.tsx b/components/main-nav.tsx new file mode 100644 index 0000000..01b8b4c --- /dev/null +++ b/components/main-nav.tsx @@ -0,0 +1,41 @@ +import * as React from "react" +import Link from "next/link" + +import { NavItem } from "@/types/nav" +import { siteConfig } from "@/config/site" +import { cn } from "@/lib/utils" +import { Icons } from "@/components/icons" + +interface MainNavProps { + items?: NavItem[] +} + +export function MainNav({ items }: MainNavProps) { + return ( +
+ + + {siteConfig.name} + + {items?.length ? ( + + ) : null} +
+ ) +} diff --git a/components/meetings/AddMeeting.tsx b/components/meetings/AddMeeting.tsx new file mode 100644 index 0000000..54ec0c1 --- /dev/null +++ b/components/meetings/AddMeeting.tsx @@ -0,0 +1,79 @@ +"use client" + +import React, { useState } from "react" +import { useRouter } from "next/navigation" +import { FilePlus2 } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { toast, useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" + +export default function AddMeeting() { + const router = useRouter() + const { toast } = useToast() + const [videoId, setvideoId] = useState("") + const [videoTitle, setVideoTitle] = useState("") + const [isLoading, setLoading] = useState(false) + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + setLoading(true) + try { + const res = await fetch("/api/salesmetting", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + videoId, + videoTitle + }), + }) + + if (res.ok) { + console.log("video added!") + toast({ + title: "video added!", + }) + setLoading(false) + window.location.reload() + } else { + console.log("user reg failed!") + toast({ + variant: "destructive", + title: "user reg failed!", + }) + setLoading(false) + } + } catch (error) { + toast({ + variant: "destructive", + title: "Error during add:" + error, + }) + console.log("Error during add:", error) + setLoading(false) + } + } + return ( +
+ setvideoId(e.target.value)} + required + /> + setVideoTitle(e.target.value)} + required + /> + +
+ ) +} diff --git a/components/meetings/MeetingList.tsx b/components/meetings/MeetingList.tsx new file mode 100644 index 0000000..938610e --- /dev/null +++ b/components/meetings/MeetingList.tsx @@ -0,0 +1,79 @@ +"use client" +import React, { useEffect, useState } from "react" +import Link from "next/link" +import { Trash2 } from "lucide-react" + +import { fetchMeetings, DeleteMeetings } from "@/lib/salesMetting/salesMetting" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { useToast } from "@/components/ui/use-toast" +import MeetingSkeleton from "./MeetingSkeleton" + +export default function MeetingList() { + const [meetingList, setMeetingList] = useState() + const { toast } = useToast() + const [isLoading, setLoading] = useState(false) + + useEffect(() => { + setLoading(true) + fetchMeetings().then((data) => { + setLoading(false) + setMeetingList(data) + }) + }, []) + + const handleDelete = async (id) => { + try { + await DeleteMeetings(id) + toast({ + variant: "default", + title: "Video deleted", + }) + window.location.reload(); + } catch (error) { + toast({ + variant: "destructive", + title: "Error during delete: " + error, + }) + console.log("Error during delete:", error) + } + } + + return ( + <> + {isLoading && } +
+ + {meetingList && + meetingList.map((singleMeeting, index) => ( +
+ +
+ +
+ +
+
+ {singleMeeting.videoTitle ? singleMeeting.videoTitle : "No title"} +
+
+ +
+ +
+
+ ))} +
+
+ + ) +} diff --git a/components/meetings/MeetingSkeleton.tsx b/components/meetings/MeetingSkeleton.tsx new file mode 100644 index 0000000..2fc317b --- /dev/null +++ b/components/meetings/MeetingSkeleton.tsx @@ -0,0 +1,49 @@ +import { Skeleton } from "@/components/ui/skeleton" +import React from 'react' + +const MeetingSkeleton = () => { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} + +export default MeetingSkeleton diff --git a/components/meetings/SalesMeeting.tsx b/components/meetings/SalesMeeting.tsx new file mode 100644 index 0000000..f511fd8 --- /dev/null +++ b/components/meetings/SalesMeeting.tsx @@ -0,0 +1,50 @@ +import React from "react" + +import { Skeleton } from "@/components/ui/skeleton" + +const SalesMeeting = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default SalesMeeting diff --git a/components/settings/ClipboardCopy.tsx b/components/settings/ClipboardCopy.tsx new file mode 100644 index 0000000..b22afa6 --- /dev/null +++ b/components/settings/ClipboardCopy.tsx @@ -0,0 +1,55 @@ +import React, { useState } from "react" +import { Copy } from "lucide-react" + +import { Button } from "../ui/button" + +export default function ClipboardCopy(props) { + const session = props.session + + const [isCopied, setIsCopied] = useState(false) + + const handleCopyClick = async () => { + try { + await navigator.clipboard.writeText( + `${process.env.NEXT_PUBLIC_NEXTAUTH_URL}/signup?ref=${ + session && (session?.user?.name).replaceAll(" ", "_") + }` + ) + setIsCopied(true) + setTimeout(() => setIsCopied(false), 2000) + } catch (error) { + console.error("Error copying to clipboard:", error) + } + } + + return ( +
+ + {process.env.NEXT_PUBLIC_NEXTAUTH_URL}/signup?ref= + {session && (session?.user?.name).replaceAll(" ", "_")} + + {/* */} + + {isCopied ? ( + + ) : ( + + )} +
+ ) +} diff --git a/components/settings/ProfileUpload.tsx b/components/settings/ProfileUpload.tsx new file mode 100644 index 0000000..29683a0 --- /dev/null +++ b/components/settings/ProfileUpload.tsx @@ -0,0 +1,131 @@ +"use client" +import { useState } from "react" +import { Check, X } from "lucide-react" +import { useSession } from "next-auth/react" +import { + Dialog +} from "@/components/ui/dialog" +import { useToast } from "@/components/ui/use-toast" +import CropImage from "@/components/CropImage" + +import { Icons } from "../icons" +import { Button } from "../ui/button" +import { Input } from "../ui/input" + +export default function ProfileUpload({ setAvatar }) { + const { toast } = useToast() + const [isLoading, setLoading] = useState(false) + const [image, setImage] = useState(null) + const [src, setSrc] = useState(null) + const [result, setResult] = useState(null) + const { data: session } = useSession() + const id = session?.user?.id + const handleImageChange = (e) => { + const file = e.target.files[0] + setImage(file) + setSrc(URL.createObjectURL(file)) + setResult(URL.createObjectURL(file)) + } + + async function updateAvatar(url) { + try { + const res = await fetch(`/api/user/avatar/?id=${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + avatar: url, + }), + }) + + if (res.ok) { + toast({ + variant: "default", + title: "User Updated!", + }) + setLoading(false) + window.location.reload() + } else { + console.log("submission failed!") + toast({ + title: "submission failed!", + }) + } + } catch (error) { + console.log("Error during submit:", error) + toast({ + variant: "destructive", + title: `Error during submit:", ${error}`, + }) + } + } + + const handleImageUpload = async () => { + setLoading(true) + const formData = new FormData() + formData.append("file", image) + formData.append("upload_preset", "p2y46g7e") + + try { + const response = await fetch( + `https://api.cloudinary.com/v1_1/drzedrk1e/image/upload`, + { + method: "POST", + body: formData, + } + ) + + if (response.ok) { + const data = await response.json() + console.log("Image uploaded successfully:", data) + updateAvatar(data.url) + } else { + console.error("Error uploading image:", response.statusText) + } + } catch (error) { + console.error("Error uploading image:", error) + } + } + + return ( +
+ {src && ( +
+
+
+ + +
+ +
+
+ )} +
+ {e.target.value=null}} + onChange={handleImageChange} + /> + +
+
+ ) +} diff --git a/components/skeleton/CardSkeleton.tsx b/components/skeleton/CardSkeleton.tsx new file mode 100644 index 0000000..a62475f --- /dev/null +++ b/components/skeleton/CardSkeleton.tsx @@ -0,0 +1,68 @@ +import React from "react" + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Skeleton } from "@/components/ui/skeleton" + +export default function CardSkeleton() { + return ( +
+ + +
+ + + + + + + + + + + + +
+
+
+ +
+ +
+
+
+
+ ) +} diff --git a/components/skeleton/CompanySkeleton.tsx b/components/skeleton/CompanySkeleton.tsx new file mode 100644 index 0000000..4f08da6 --- /dev/null +++ b/components/skeleton/CompanySkeleton.tsx @@ -0,0 +1,41 @@ +import React from "react" + +import { Skeleton } from "@/components/ui/skeleton" + +const CompanySkeleton = () => { + return ( +
+ +
+ + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + +
+ ) +} + +export default CompanySkeleton diff --git a/components/skeleton/MainSkeleton.tsx b/components/skeleton/MainSkeleton.tsx new file mode 100644 index 0000000..2a2ffb6 --- /dev/null +++ b/components/skeleton/MainSkeleton.tsx @@ -0,0 +1,137 @@ +import React from "react"; +import CardSkeleton from "@/components/skeleton/CardSkeleton" +import { Skeleton } from "@/components/ui/skeleton" +const MainSkeleton = () => { + return <> +
+ + + + +
+
+ + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+; +}; + +export default MainSkeleton; diff --git a/components/skeleton/MangeUser.tsx b/components/skeleton/MangeUser.tsx new file mode 100644 index 0000000..4ef2a87 --- /dev/null +++ b/components/skeleton/MangeUser.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { Skeleton } from "@/components/ui/skeleton" +const MangeUser = () => { + return
+
+ +
+ + +
+
+ + + + + + + +
+
+ + + + + + + + + +
+
+ + +
+
+
; +}; + +export default MangeUser; diff --git a/components/skeleton/Sales2Skeleton.tsx b/components/skeleton/Sales2Skeleton.tsx new file mode 100644 index 0000000..f7e57bd --- /dev/null +++ b/components/skeleton/Sales2Skeleton.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Skeleton } from "@/components/ui/skeleton" +const Sales2Skeleton = () => { + return
+ + + + + + +
+
+ + + +
+ +
+
+ + + + + +
+
+ + + + + + + + + +
+
+ + +
+
; +}; + +export default Sales2Skeleton; diff --git a/components/statusBadge.tsx b/components/statusBadge.tsx new file mode 100644 index 0000000..de90a57 --- /dev/null +++ b/components/statusBadge.tsx @@ -0,0 +1,51 @@ +import React from "react" + +import { Badge } from "./ui/badge" + +export default function Statusbadge(props) { + const value = props.value + let styles + switch (value) { + case "On Going": + styles = + "rounded-full px-1.5 border-[#3AAE54] bg-[#E7FBF0] text-[#3AAE54] w-[88px] justify-center font-normal" + break + case "On Hold": + styles = + "rounded-full px-1.5 border-[#F95959] bg-[#FFEEEE] text-[#F95959] w-[88px] justify-center font-normal" + break + case "Pending": + styles = + "rounded-full px-1.5 border-[#F2994A] bg-[#FFF8F2] text-[#F2994A] w-[88px] justify-center font-normal" + break + case "Complete": + styles = + "rounded-full px-1.5 border-[#878790] bg-[#878790] text-white w-[88px] justify-center font-normal" + break + case "Paid": + styles = + "rounded-full px-1.5 border-[#3AAE54] bg-[#E7FBF0] text-[#3AAE54] w-[88px] justify-center font-normal" + break + case "Yes": + styles = + "rounded-full px-1.5 border-[#3AAE54] bg-[#E7FBF0] text-[#3AAE54] w-[88px] justify-center font-normal" + break + case "Unpaid": + styles = + "rounded-full px-1.5 border-[#F95959] bg-[#FFEEEE] text-[#F95959] w-[88px] justify-center font-normal" + break + case "No": + styles = + "rounded-full px-1.5 border-[#F95959] bg-[#FFEEEE] text-[#F95959] w-[88px] justify-center font-normal" + break + default: + styles = "" + break + } + + return ( + + {value} + + ) +} diff --git a/components/superAdmin/.DS_Store b/components/superAdmin/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/components/superAdmin/.DS_Store differ diff --git a/components/superAdmin/EditClient.tsx b/components/superAdmin/EditClient.tsx new file mode 100644 index 0000000..4d4ff7d --- /dev/null +++ b/components/superAdmin/EditClient.tsx @@ -0,0 +1,198 @@ +"use client" + +import React, { FormEvent, useEffect, useState } from "react" +import { Checkbox } from "@/components/ui/checkbox" +import { + Calendar as CalendarIcon, + Check, + ChevronsUpDown, + FileEdit, + PlusCircle, +} from "lucide-react" + +import { fetchCompanies } from "@/lib/company/company" +import { cn } from "@/lib/utils" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" +import SingleCompany from "@/app/company/[name]/page" + +import { fetchUsers } from "../../lib/fetchUsers" +import { Button } from "../ui/button" + +export default function EditClient(props) { + const [open, setOpen] = useState(false) + const [value, setValue] = useState("") + + const { toast } = useToast() + const clientId = props.id + const [isLoading, setLoading] = useState(false) + const [clientName, setClientName] = useState("") + const [email, setEmail] = useState("") + const [phone, setPhone] = useState("") + const [address, setAddress] = useState("") + const [callClient, setIsClientCall] = useState() + const [emailClient, setIsClientEmail] = useState() + + useEffect(() => { + fetch(`/api/client/?id=${clientId}`) + .then((response) => response.json()) + .then((data) => { + const clientData = data.client + setClientName(clientData.clientName) + setEmail(clientData.email) + setPhone(clientData.phone) + setAddress(clientData.address) + setIsClientCall(clientData.callClient) + setIsClientEmail(clientData.emailClient) + }) + .catch((error) => { + console.error("Error:", error) + }) + }, [clientId]) + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + setLoading(true) + try { + const res = await fetch(`/api/client/?id=${clientId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + clientName, + email, + phone, + address, + callClient, + emailClient + }), + }) + + if (res.ok) { + setLoading(false) + toast({ + variant: "default", + title: "Client Updated!", + }) + window.location.reload() + } else { + toast({ + variant: "destructive", + title: "submission failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during submit:", error) + toast({ + variant: "destructive", + title: `Error during submit:", ${error}`, + }) + setLoading(false) + } + } + + return ( + + + + + + + Edit Client + +
+
+ + setClientName(e.target.value.trim())} + /> + + setEmail(e.target.value.trim())} + /> + + setPhone(e.target.value.trim())} + /> + + +
+
+ setIsClientCall(!callClient)} + /> + +
+
+ setIsClientEmail(!emailClient)} + /> + +
+
+ + +
+
+
+
+
+
+ ) +} diff --git a/components/superAdmin/EditUser.tsx b/components/superAdmin/EditUser.tsx new file mode 100644 index 0000000..03b3a80 --- /dev/null +++ b/components/superAdmin/EditUser.tsx @@ -0,0 +1,293 @@ +"use client" + +import React, { FormEvent, useEffect, useState } from "react" +import { + Calendar as CalendarIcon, + Check, + ChevronsUpDown, + FileEdit, + PlusCircle, +} from "lucide-react" + +import { fetchCompanies } from "@/lib/company/company" +import { cn } from "@/lib/utils" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" +import SingleCompany from "@/app/company/[name]/page" + +import { fetchUsers } from "../../lib/fetchUsers" +import { Button } from "../ui/button" + +export default function EditUser(props) { + const [open, setOpen] = useState(false) + const [value, setValue] = useState(null) + + const { toast } = useToast() + const userId = props.id + const [isLoading, setLoading] = useState(false) + const [userData, setuserData] = useState() + + const [Name, setName] = useState() + const [Role, setRole] = useState("") + const [commission_rate, setcommission_rate] = useState(null) + const [upSeller, setupSeller] = useState() + const [upSellerId, setupSellerId] = useState() + const [Users, setUsers] = useState() + + const [adminCompany, setadminCompany] = useState() + + const [activeCompany, setactiveCompany] = useState() + const [offerCompany, setofferCompany] = useState() + + const [NewCompany, setNewCompany] = useState() + const [selectedRate, setSelectedRate] = useState(null) + const [selectedLogo, setSelectedLogo] = useState(null) + + useEffect(() => { + fetch(`/api/user/?id=${userId}`) + .then((response) => response.json()) + .then((data) => { + const userData = data.user + setuserData(userData) + setValue(userData?.upSellerId) + setRole(userData?.role) + setcommission_rate(userData?.upsellerPercentage) + setupSellerId(userData?.upSellerId) + const companyNamesArray = userData.contracts.map( + (contract) => contract.companyName + ) + setactiveCompany(companyNamesArray) + }) + .catch((error) => { + console.error("Error:", error) + }) + + fetchUsers("Sales1") + .then((users) => { + setUsers(users) + }) + .catch((error) => { + console.error("Error in component:", error) + }) + + fetchCompanies().then((companies) => { + setofferCompany(companies) + }) + }, []) + + if (!userData) { + return + } + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + + setLoading(true) + try { + const res = await fetch(`/api/user/?id=${userId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: Name, + role: Role, + commission_rate, + upSeller, + upSellerId, + }), + }) + + if (res.ok) { + setLoading(false) + toast({ + variant: "default", + title: "User Updated!", + }) + window.location.reload() + } else { + console.log("submission failed!") + toast({ + title: "submission failed!", + }) + setLoading(false) + } + } catch (error) { + console.log("Error during submit:", error) + toast({ + variant: "destructive", + title: `Error during submit:", ${error}`, + }) + setLoading(false) + } + } + + return ( + + + + + + + Edit User + +
+
+ <> + + setName(e.target.value.trim())} + /> + +
+ +
+ {console.log(Role)} + {Users && (Role!=="Admin-IA")&&( + <> + +
+ + + + + + + + No upseller found. + + {setupSeller("none") + setupSellerId(null),setOpen(false),setValue("")}} value="none"> + + None + + {Users.map((user) => ( + { + setValue( + currentValue === value ? "" : currentValue + ) + setupSeller(user.name) + setupSellerId(currentValue) + setOpen(false) + }} + > + + {user.name} + + ))} + + + + +
+ + {upSellerId && +
+ + setcommission_rate(e.target.value.trim())} + /> +
+ } + + + )} + + + {Role && ( + <> + + setadminCompany(e.target.value.trim())} + /> + + )} + + +
+
+
+
+
+
+ ) +} diff --git a/components/superAdmin/TotalClients.tsx b/components/superAdmin/TotalClients.tsx new file mode 100644 index 0000000..5b1c3c5 --- /dev/null +++ b/components/superAdmin/TotalClients.tsx @@ -0,0 +1,38 @@ +"use client" +import React, { useEffect, useState } from "react" + +import { fetchProjectsBySales } from "@/lib/fetchProjects" + +export default function TotalClients(props) { + const [count, setcount] = useState() + + const userName = props.name + + useEffect(()=>{ + fetch("/api/project?salesperson=" + userName) + .then((response) => response.json()) + .then((data) => { + const projects = data.project + + const uniqueClientNames = [] + + for (const project of projects) { + const clientName = project.clientName + if (!uniqueClientNames.includes(clientName)) { + uniqueClientNames.push(clientName) + } + } + + setcount(uniqueClientNames.length) + }) + .catch((error) => { + console.error("Error:", error) + }) + },[userName]) + + + if (!count) { + return 0 + } + return
{count}
+} diff --git a/components/superAdmin/TotalEarning.tsx b/components/superAdmin/TotalEarning.tsx new file mode 100644 index 0000000..576d202 --- /dev/null +++ b/components/superAdmin/TotalEarning.tsx @@ -0,0 +1,30 @@ +import React, { useEffect, useState } from "react" +import { InvoiceByUserId } from "@/lib/fetchInvoices" +export default function TotalEarning(props) { + const userId = props.userId + const [invoices, setInvoices] = useState(0) + + function formatNumber(number) { + if (number >= 1e9) { + return (number / 1e9).toFixed(1) + "B" + } else if (number >= 1e6) { + return (number / 1e6).toFixed(1) + "M" + } else if (number >= 1e3) { + return (number / 1e3).toFixed(1) + "K" + } + return number.toString() + } + + + useEffect(() => { + InvoiceByUserId(userId).then((res)=>{ + setInvoices(res) + }) + }, [userId]) + + if (!invoices) { + return 0 + } + + return
{formatNumber(invoices?.toFixed(1))}
+} diff --git a/components/superAdmin/TotalProject.tsx b/components/superAdmin/TotalProject.tsx new file mode 100644 index 0000000..bc5b9e3 --- /dev/null +++ b/components/superAdmin/TotalProject.tsx @@ -0,0 +1,23 @@ +"use client" +import React, { useEffect, useState } from "react" + +export default function TotalEarning(props) { + const [count, setcount] = useState() + + const userName = props.name + useEffect(()=>{ + fetch(`/api/project?salesperson=${userName}`) + .then((res) => res.json()) + .then((user) => { + setcount(user.project.length) + }) + .catch((error) => { + console.error("Error:", error) + }) + },[userName]) + if (!count) { + return 0 + } + + return <>{count} +} diff --git a/components/superAdmin/TotalSales.tsx b/components/superAdmin/TotalSales.tsx new file mode 100644 index 0000000..d681900 --- /dev/null +++ b/components/superAdmin/TotalSales.tsx @@ -0,0 +1,18 @@ +import React, { useEffect, useState } from "react" + +export default function TotalSales(props) { + const userId = props.userId + const [totalSales, settotalSales] = useState() + useEffect(() => { + fetch("/api/user?upSellerId=" + userId) + .then((response) => response.json()) + .then((data) => { + settotalSales(data.users.length) + }) + .catch((error) => { + console.error("Error:", error) + }) + }, [userId]) + + return
{totalSales && totalSales}
+} diff --git a/components/superAdmin/ViewClient.tsx b/components/superAdmin/ViewClient.tsx new file mode 100644 index 0000000..557166a --- /dev/null +++ b/components/superAdmin/ViewClient.tsx @@ -0,0 +1,151 @@ +"use client" + +import React, { useEffect, useState } from "react" +import { + Calendar as CalendarIcon, + Check, + ChevronsUpDown, + Eye, + FileEdit, + PlusCircle, +} from "lucide-react" + +import { fetchCompanies } from "@/lib/company/company" +import { cn } from "@/lib/utils" +import { Checkbox } from "@/components/ui/checkbox" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from "@/components/ui/command" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Skeleton } from "@/components/ui/skeleton" +import { useToast } from "@/components/ui/use-toast" +import { Icons } from "@/components/icons" +import SingleCompany from "@/app/company/[name]/page" + +import { fetchUsers } from "../../lib/fetchUsers" +import { Button } from "../ui/button" + +export default function ViewClient(props) { + const [open, setOpen] = useState(false) + const [value, setValue] = useState("") + + const { toast } = useToast() + const clientId = props.id + const [isLoading, setLoading] = useState(false) + const [client, setClient] = useState({}) + + useEffect(() => { + setLoading(true) + fetch(`/api/client/?id=${clientId}`) + .then((response) => response.json()) + .then((data) => { + setLoading(false) + const clientData = data.client + setClient(clientData) + }) + .catch((error) => { + console.error("Error:", error) + }) + }, [clientId]) + + return ( + + + + + + {isLoading ? ( +
+ + + + + + +
+ ) : ( + + + View Client + + + +
+
+ +

{client?.clientName}

+ +

{client?.email}

+ +

{client?.phone}

+ +

{client?.address}

+
+
+ + +
+
+ + +
+
+
+
+
+
+ )} +
+
+ ) +} diff --git a/components/tailwind-indicator.tsx b/components/tailwind-indicator.tsx new file mode 100644 index 0000000..822e0a3 --- /dev/null +++ b/components/tailwind-indicator.tsx @@ -0,0 +1,14 @@ +export function TailwindIndicator() { + if (process.env.NODE_ENV === "production") return null + + return ( +
+
xs
+
sm
+
md
+
lg
+
xl
+
2xl
+
+ ) +} diff --git a/components/toolkitMenu.tsx b/components/toolkitMenu.tsx new file mode 100644 index 0000000..ee81431 --- /dev/null +++ b/components/toolkitMenu.tsx @@ -0,0 +1,183 @@ +"use client" + +import React from "react" +import Link from "next/link" + +export default function toolkitMenu() { + return ( +
+
+ + + +
+
+ + Sales Toolkit + +
    +
  • + + + + + Email Signature + +
  • +
  • + + + + + Email Setup + +
  • +
  • + + + + + Pricing + +
  • +
  • + + + + + Website + +
  • +
+
+ {/* + + + + + + Email Signature + + + + + + Email Setup + + + + + + Pricing + + + + + + Website + + + */} +
+ ) +} diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..c3dceec --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,143 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = ({ + className, + ...props +}: AlertDialogPrimitive.AlertDialogPortalProps) => ( + +) +AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..13ea882 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-black dark:border-destructive [&>svg]:text-black", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..382b8ce --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..aed8025 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-[#0000]/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "underline-offset-4 hover:underline text-primary", + }, + size: { + default: "h-10 py-2 px-4", + sm: "h-9 px-3 rounded-md", + lg: "h-11 px-8 rounded-md", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx new file mode 100644 index 0000000..7448ded --- /dev/null +++ b/components/ui/calendar.tsx @@ -0,0 +1,62 @@ +import * as React from "react" +import { ChevronLeft, ChevronRight } from "lucide-react" +import { DayPicker } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +export type CalendarProps = React.ComponentProps + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + , + IconRight: ({ ...props }) => , + }} + {...props} + /> + ) +} +Calendar.displayName = "Calendar" + +export { Calendar } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx new file mode 100644 index 0000000..4cba6c5 --- /dev/null +++ b/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/components/ui/command.tsx b/components/ui/command.tsx new file mode 100644 index 0000000..3301b06 --- /dev/null +++ b/components/ui/command.tsx @@ -0,0 +1,153 @@ +import * as React from "react" +import { DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..5ee6ad5 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,121 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = ({ + className, + ...props +}: DialogPrimitive.DialogPortalProps) => ( + +) +DialogPortal.displayName = DialogPrimitive.Portal.displayName + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..4454653 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..4603f8b --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +