diff --git a/client/.env b/client/.env deleted file mode 100644 index 57f426c..0000000 --- a/client/.env +++ /dev/null @@ -1,4 +0,0 @@ -VITE_API_URL=http://localhost:3001 -# VITE_API_URL=http://192.168.1.171:3001 -# VITE_API_URL=https://crm.stream.graff.tech/api -VITE_STREAM_URL=https://stream.graff.tech diff --git a/client/.env.development b/client/.env.development new file mode 100644 index 0000000..e1eb4c9 --- /dev/null +++ b/client/.env.development @@ -0,0 +1,2 @@ +VITE_API_URL=http://localhost:3001 +VITE_STREAM_URL=https://stream.graff.tech diff --git a/client/.env.production b/client/.env.production new file mode 100644 index 0000000..bef2196 --- /dev/null +++ b/client/.env.production @@ -0,0 +1,2 @@ +VITE_API_URL=https://crm.stream.graff.tech/api +VITE_STREAM_URL=https://stream.graff.tech diff --git a/client/src/components/Card.tsx b/client/src/components/Card.tsx index a31e0f3..2c5fd0e 100644 --- a/client/src/components/Card.tsx +++ b/client/src/components/Card.tsx @@ -97,7 +97,7 @@ function Card({
{manager ? ( diff --git a/client/src/components/Input.tsx b/client/src/components/Input.tsx index 5cae857..2237bb8 100644 --- a/client/src/components/Input.tsx +++ b/client/src/components/Input.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; interface InputProps { type?: "text" | "email" | "password" | "time" | "tel"; + value?: string; placeholder?: string; autoFocus?: boolean; required?: boolean; @@ -16,11 +17,12 @@ interface InputProps { function Input({ type = "text", + value, placeholder, autoFocus, required, readOnly, - defaultValue, + defaultValue = "", className, handleChange, handleFocus, @@ -30,11 +32,15 @@ function Input({ replacement: { _: /\d/ }, }); - const [value, setValue] = useState(defaultValue || ""); + const [_value, setValue] = useState(value || defaultValue || ""); useEffect(() => { - handleChange && handleChange(value); - }, [value]); + if (!_value || !handleChange) { + return; + } + + handleChange(_value); + }, [_value]); return ( setValue(e.target.value)} onFocus={handleFocus} className={`px-3 py-2.5 outline-none rounded-lg border border-[#DAE0E5] focus:border-[#49A1F5] transition-colors text-sm ${className}`} diff --git a/client/src/components/Managers.tsx b/client/src/components/Managers.tsx index 80ee7fb..e74fe35 100644 --- a/client/src/components/Managers.tsx +++ b/client/src/components/Managers.tsx @@ -30,7 +30,7 @@ function Managers() {
diff --git a/client/src/components/SelectUser.tsx b/client/src/components/SelectUser.tsx index dbdb106..5f2174a 100644 --- a/client/src/components/SelectUser.tsx +++ b/client/src/components/SelectUser.tsx @@ -36,10 +36,7 @@ function SelectUser({ >
diff --git a/client/src/components/modals/CreateBuildModal.tsx b/client/src/components/modals/CreateBuildModal.tsx new file mode 100644 index 0000000..b0b37fa --- /dev/null +++ b/client/src/components/modals/CreateBuildModal.tsx @@ -0,0 +1,84 @@ +import { FormEvent, useState } from "react"; +import Button from "../Button"; +import Input from "../Input"; +import api from "../../utils/api"; +import IError from "../../types/IError"; +import IBuild from "../../types/IBuild"; +import useModalStore from "../../stores/useModalStore"; + +interface Props { + companyId: string; +} + +function CreateBuildModal({ companyId }: Props) { + const [name, setName] = useState(""); + const [build, setBuild] = useState(""); + const [sessionLimit, setSessionLimit] = useState(1); + const { setModal } = useModalStore(); + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + + addBuild(); + } + + async function addBuild() { + try { + const result: IBuild | IError = await api + .post(`admin/builds`, { + json: { + companyId, + name, + build, + sessionLimit, + }, + }) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + window.location.reload(); + } catch (error) { + alert((error as Error).message); + } + } + + return ( +
+

Создание ЖК

+
+ setName(value)} + autoFocus + required + /> + setBuild(value)} + required + /> + setSessionLimit(+value)} + required + /> +
+ + +
+
+
+ ); +} + +export default CreateBuildModal; diff --git a/client/src/components/modals/CreateCompanyModal.tsx b/client/src/components/modals/CreateCompanyModal.tsx new file mode 100644 index 0000000..f80f003 --- /dev/null +++ b/client/src/components/modals/CreateCompanyModal.tsx @@ -0,0 +1,62 @@ +import { FormEvent, useState } from "react"; +import Button from "../Button"; +import Input from "../Input"; +import api from "../../utils/api"; +import ICompany from "../../types/ICompany"; +import IError from "../../types/IError"; +import useModalStore from "../../stores/useModalStore"; + +function CreateCompanyModal() { + const [name, setName] = useState(""); + const { setModal } = useModalStore(); + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + + addComapny(); + } + + async function addComapny() { + try { + const result: ICompany | IError = await api + .post(`admin/companies`, { + json: { + name, + }, + }) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + window.location.reload(); + } catch (error) { + alert((error as Error).message); + } + } + + return ( +
+

Создание компании

+
+ setName(value)} + autoFocus + required + /> +
+ + +
+
+
+ ); +} + +export default CreateCompanyModal; diff --git a/client/src/components/modals/CreateUserModal.tsx b/client/src/components/modals/CreateUserModal.tsx new file mode 100644 index 0000000..360522d --- /dev/null +++ b/client/src/components/modals/CreateUserModal.tsx @@ -0,0 +1,159 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { ChangeEvent, FormEvent, useEffect, useState } from "react"; +import Button from "../Button"; +import Input from "../Input"; +import api from "../../utils/api"; +import IError from "../../types/IError"; +import IBuild from "../../types/IBuild"; +import { Transition } from "react-transition-group"; +import SpinnerIcon from "../icons/SpinnerIcon"; +import useModalStore from "../../stores/useModalStore"; + +interface Props { + companyId: string; +} + +function CreateUserModal({ companyId }: Props) { + const [name, setName] = useState(""); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [builds, setBuilds] = useState(); + const [role, setRole] = useState("manager"); + const [buildIds, setBuildIds] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { setModal } = useModalStore(); + + async function getBuilds() { + try { + const result: IBuild[] | IError = await api + .get(`admin/builds?companyId=${companyId}`) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + setBuilds(result); + } catch (error) { + alert((error as Error).message); + } + } + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + + addUser(); + } + + async function addUser() { + try { + const result: IBuild | IError = await api + .post(`admin/users`, { + json: { + companyId, + name, + username, + password, + role, + buildIds, + }, + }) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + window.location.reload(); + } catch (error) { + alert((error as Error).message); + } + } + + function handleChangeBuilds(e: ChangeEvent) { + const buildValues = [...e.target.selectedOptions].map( + (option) => option.value + ); + + setBuildIds(buildValues); + } + + useEffect(() => { + getBuilds(); + }, []); + + useEffect(() => { + if (!builds) { + return; + } + + setIsLoading(false); + }, [builds]); + + return ( +
+

Создание пользователя

+
+ setName(value)} + autoFocus + required + /> + setUsername(value)} + required + /> + setPassword(value)} + required + /> + + +
+ + +
+
+ + + {(state) => ( +
+ +
+ )} +
+
+ ); +} + +export default CreateUserModal; diff --git a/client/src/components/modals/EditUserModal.tsx b/client/src/components/modals/EditUserModal.tsx new file mode 100644 index 0000000..aeb334e --- /dev/null +++ b/client/src/components/modals/EditUserModal.tsx @@ -0,0 +1,184 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { ChangeEvent, FormEvent, useEffect, useState } from "react"; +import Button from "../Button"; +import Input from "../Input"; +import api from "../../utils/api"; +import IError from "../../types/IError"; +import IBuild from "../../types/IBuild"; +import { Transition } from "react-transition-group"; +import SpinnerIcon from "../icons/SpinnerIcon"; +import IUser from "../../types/IUser"; +import useModalStore from "../../stores/useModalStore"; + +interface Props { + companyId: string; + userId: string; +} + +function EditUserModal({ companyId, userId }: Props) { + const [user, setUser] = useState(); + const [name, setName] = useState(""); + const [username, setUsername] = useState(""); + // const [password, setPassword] = useState(""); + const [builds, setBuilds] = useState(); + const [role, setRole] = useState("manager"); + const [buildIds, setBuildIds] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { setModal } = useModalStore(); + + async function getUser() { + try { + const result: IUser | IError = await api + .get(`admin/users/${userId}`) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + setUser(result); + setName(result.name); + setUsername(result.username); + setRole(result.role); + + if (result.buildIds) { + setBuildIds(result.buildIds); + } + } catch (error) { + alert((error as Error).message); + } + } + + async function getBuilds() { + try { + const result: IBuild[] | IError = await api + .get(`admin/builds?companyId=${companyId}`) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + setBuilds(result); + } catch (error) { + alert((error as Error).message); + } + } + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + + updateUser(); + } + + async function updateUser() { + try { + const result: IBuild | IError = await api + .put(`admin/users/${userId}`, { + json: { + name, + username, + role, + buildIds, + }, + }) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + window.location.reload(); + } catch (error) { + alert((error as Error).message); + } + } + + function handleChangeBuilds(e: ChangeEvent) { + const buildValues = [...e.target.selectedOptions].map( + (option) => option.value + ); + + setBuildIds(buildValues); + } + + useEffect(() => { + getUser(); + getBuilds(); + }, []); + + useEffect(() => { + if (!user || !builds || !buildIds) { + return; + } + + setIsLoading(false); + }, [user, builds, buildIds]); + + return ( +
+

Редактирование

+
+ setName(value)} + required + /> + setUsername(value)} + required + /> + + +
+ + +
+
+ + + {(state) => ( +
+ +
+ )} +
+
+ ); +} + +export default EditUserModal; diff --git a/client/src/main.tsx b/client/src/main.tsx index 665a620..910fe61 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -8,6 +8,8 @@ import LoginPage from "./pages/LoginPage.tsx"; import RegistrationPage from "./pages/RegistrationPage.tsx"; import RegistrationCompanyPage from "./pages/RegistrationCompanyPage.tsx"; import RegistrationManagerPage from "./pages/RegistrationManagerPage.tsx"; +import AdminPage from "./pages/AdminPage.tsx"; +import AdminCompanyPage from "./pages/AdminCompanyPage.tsx"; const router = createBrowserRouter([ { @@ -48,6 +50,25 @@ const router = createBrowserRouter([ }, ], }, + { + path: "admin", + element: , + children: [ + { + index: true, + element: , + }, + { + path: ":companyId", + children: [ + { + index: true, + element: , + }, + ], + }, + ], + }, ], }, ]); diff --git a/client/src/pages/AdminCompanyPage.tsx b/client/src/pages/AdminCompanyPage.tsx new file mode 100644 index 0000000..3943c72 --- /dev/null +++ b/client/src/pages/AdminCompanyPage.tsx @@ -0,0 +1,194 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useEffect, useState } from "react"; +import api from "../utils/api"; +import IError from "../types/IError"; +import Button from "../components/Button"; +import useModalStore from "../stores/useModalStore"; +import ModalContainer from "../components/ModalContainer"; +import { useNavigate, useParams } from "react-router-dom"; +import IBuild from "../types/IBuild"; +import CreateBuildModal from "../components/modals/CreateBuildModal"; +import IUser from "../types/IUser"; +import CreateUserModal from "../components/modals/CreateUserModal"; +import { Transition } from "react-transition-group"; +import SpinnerIcon from "../components/icons/SpinnerIcon"; +import MoreIcon from "../components/icons/MoreIcon"; +import EditUserModal from "../components/modals/EditUserModal"; + +function AdminCompanyPage() { + const { companyId } = useParams(); + const [builds, setBuilds] = useState(); + const [users, setUsers] = useState(); + const { setModal } = useModalStore(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + + async function getBuilds() { + if (!companyId) return; + + try { + const result: IBuild[] | IError = await api + .get(`admin/builds?companyId=${companyId}`) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + setBuilds(result); + } catch (error) { + alert((error as Error).message); + } + } + + async function getUsers() { + if (!companyId) return; + + try { + const result: IUser[] | IError = await api + .get(`admin/users?companyId=${companyId}`) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + setUsers(result); + } catch (error) { + alert((error as Error).message); + } + } + + useEffect(() => { + getBuilds(); + getUsers(); + }, []); + + useEffect(() => { + if (!users || !builds) { + return; + } + + setIsLoading(false); + }, [users, builds]); + + return ( +
+
+ +
+
+
+

Жилые комплексы

+ +
+
+ {builds && + builds.map((build) => ( +
+ +
+

{build.name}

+

+ Сборка приложения:{" "} + {build.build} +

+

+ Кол-во слотов:{" "} + {build.sessionLimit} +

+
+
+ ))} +
+
+
+
+

Пользователи

+ +
+
+ {users && + users.map((user) => ( +
+
+ + +
+

{user.name}

+

{user.username}

+

Роль: {user.role}

+

+ Связанные ЖК:{" "} + {builds + ?.filter((build) => user.buildIds?.includes(build.id)) + .map((build) => build.name) + .join(", ") || "Нет связанных сборок"} +

+
+
+ +
+ ))} +
+
+ + + + + {(state) => ( +
+ +
+ )} +
+
+ ); +} + +export default AdminCompanyPage; diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx new file mode 100644 index 0000000..08f3ce6 --- /dev/null +++ b/client/src/pages/AdminPage.tsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; +import ICompany from "../types/ICompany"; +import api from "../utils/api"; +import IError from "../types/IError"; +import Button from "../components/Button"; +import useModalStore from "../stores/useModalStore"; +import CreateCompanyModal from "../components/modals/CreateCompanyModal"; +import ModalContainer from "../components/ModalContainer"; +import { useNavigate } from "react-router-dom"; +import SpinnerIcon from "../components/icons/SpinnerIcon"; +import { Transition } from "react-transition-group"; + +function AdminPage() { + const [companies, setCompanies] = useState(); + const { setModal } = useModalStore(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + + async function getCompanies() { + try { + const result: ICompany[] | IError = await api + .get(`admin/companies`) + .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + setCompanies(result); + } catch (error) { + alert((error as Error).message); + } + } + + useEffect(() => { + getCompanies(); + }, []); + + useEffect(() => { + if (!companies) { + return; + } + + setIsLoading(false); + }, [companies]); + + return ( +
+
+

Компании

+ +
+
+ {companies && + companies.map((company) => ( + + ))} +
+ + + + + {(state) => ( +
+ +
+ )} +
+
+ ); +} + +export default AdminPage; diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx index dac4886..9f57945 100644 --- a/client/src/pages/DashboardPage.tsx +++ b/client/src/pages/DashboardPage.tsx @@ -27,6 +27,8 @@ import SpinnerIcon from "../components/icons/SpinnerIcon"; import ModalContainer from "../components/ModalContainer"; import ChevronLeftIcon from "../components/icons/ChevronLeftIcon"; import ChevronRightIcon from "../components/icons/ChevronRightIcon"; +import IUser from "../types/IUser"; +import IError from "../types/IError"; function DashboardPage() { const { user } = useAuthStore(); @@ -69,7 +71,7 @@ function DashboardPage() { } function findSessionsByTime(time: Date) { - if (!scheduledSessions) return []; + if (!scheduledSessions || !scheduledSessions.length) return []; const foundScheduledSessions = scheduledSessions?.filter( (scheduledSession) => isEqual(new Date(scheduledSession.startAt), time) @@ -180,14 +182,20 @@ function DashboardPage() { } try { - const result: any = await api - .get(`companies/${company.id}/builds/${selectedBuild.id}/users`) + const result: IUser[] | IError = await api + .get(`users?companyId=${company.id}&buildIds=${selectedBuild.id}`) .json(); + + if ("error" in result) { + alert(result.error); + return; + } + + console.log("result", result); + setManagers(result); } catch (error) { - if (error instanceof Error) { - console.log("Error: ", error.message); - } + alert((error as Error).message); } } diff --git a/client/src/types/IError.ts b/client/src/types/IError.ts new file mode 100644 index 0000000..02779e8 --- /dev/null +++ b/client/src/types/IError.ts @@ -0,0 +1,5 @@ +interface IError { + error: string; +} + +export default IError; diff --git a/client/src/types/IUser.ts b/client/src/types/IUser.ts index ecff65a..52b9965 100644 --- a/client/src/types/IUser.ts +++ b/client/src/types/IUser.ts @@ -1,10 +1,11 @@ interface IUser { id: string; companyId: string; - avatar: string; + avatar?: string; name: string; username: string; role: string; + buildIds?: string[]; } export default IUser; diff --git a/server/ecosystem.config.js b/server/ecosystem.config.cjs similarity index 100% rename from server/ecosystem.config.js rename to server/ecosystem.config.cjs diff --git a/server/package.json b/server/package.json index 992f312..23c8b8b 100644 --- a/server/package.json +++ b/server/package.json @@ -2,10 +2,11 @@ "name": "server", "private": true, "version": "0.0.0", + "type": "module", "scripts": { - "dev": "nodemon src/index.ts", - "build": "npx tsc", - "start": "node dist/index.js" + "dev": "nodemon --exec node --import=./register.js ./src/index.ts", + "build": "npx tsc -p ./", + "start": "node ./dist/index.js" }, "dependencies": { "bcrypt": "^5.1.1", diff --git a/server/register.js b/server/register.js new file mode 100644 index 0000000..2134bb0 --- /dev/null +++ b/server/register.js @@ -0,0 +1,4 @@ +import { register } from "module"; +import { pathToFileURL } from "url"; + +register("ts-node/esm", pathToFileURL("./")); diff --git a/server/src/index.ts b/server/src/index.ts index 34ce590..714f9f0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,22 +1,25 @@ import "dotenv/config"; import express, { json } from "express"; -import connectDB from "./config/db"; +import connectDB from "./config/db.js"; import cors from "cors"; -import loginRouter from "./routes/login"; -import registrationRouter from "./routes/registration"; -import authMiddleware from "./middlewares/auth"; -import companiesRouter from "./routes/companies"; -import usersRouter from "./routes/users"; -import buildsRouter from "./routes/builds"; -import actionsRouter from "./routes/actions"; -import schedulesRouter from "./routes/schedules"; -import scheduledSessionsRoute from "./routes/scheduledSessions"; +import loginRouter from "./routes/login.js"; +import registrationRouter from "./routes/registration.js"; +import authMiddleware from "./middlewares/auth.js"; +import companiesRouter from "./routes/companies.js"; +import usersRouter from "./routes/users.js"; +import buildsRouter from "./routes/builds.js"; +import actionsRouter from "./routes/actions.js"; +import schedulesRouter from "./routes/schedules.js"; +import scheduledSessionsRoute from "./routes/scheduledSessions.js"; +import adminCompaniesRoute from "./routes/admin/adminCompaniesRoute.js"; +import adminBuildsRoute from "./routes/admin/adminBuildsRoute.js"; +import adminUsersRoute from "./routes/admin/adminUsersRoute.js"; + +await connectDB(); const app = express(); const port = process.env.PORT || 3000; -connectDB(); - app.use(json()); app.use(cors({ origin: "*" })); @@ -28,6 +31,9 @@ app.use("/scheduled_sessions", scheduledSessionsRoute); app.use("/schedules", schedulesRouter); app.use("/companies", authMiddleware, companiesRouter); app.use("/users", authMiddleware, usersRouter); +app.use("/admin/companies", adminCompaniesRoute); +app.use("/admin/builds", adminBuildsRoute); +app.use("/admin/users", adminUsersRoute); app.listen(port, () => { console.log(`Server listening on port ${port}`); diff --git a/server/src/middlewares/auth.ts b/server/src/middlewares/auth.ts index 7f2398d..bbfdb18 100644 --- a/server/src/middlewares/auth.ts +++ b/server/src/middlewares/auth.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import jwt, { Secret } from "jsonwebtoken"; -import Token from "../models/Token"; -import User from "../models/User"; +import Token from "../models/Token.js"; +import User from "../models/User.js"; async function authMiddleware(req: Request, res: Response, next: NextFunction) { if (!req.headers.authorization || !req.headers.authorization.split(" ")[1]) { diff --git a/server/src/models/BuildUser.ts b/server/src/models/BuildUser.ts deleted file mode 100644 index 04ff551..0000000 --- a/server/src/models/BuildUser.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { model, Schema } from "mongoose"; - -const buildUserSchema = new Schema( - { - buildId: { - type: Schema.Types.ObjectId, - ref: "Build", - required: true, - }, - userId: { - type: Schema.Types.ObjectId, - ref: "User", - required: true, - }, - }, - { - timestamps: true, - toJSON: { virtuals: true }, - toObject: { virtuals: true }, - } -); - -const BuildUser = model("Build_User", buildUserSchema); - -export default BuildUser; diff --git a/server/src/models/User.ts b/server/src/models/User.ts index d514d52..9d677be 100644 --- a/server/src/models/User.ts +++ b/server/src/models/User.ts @@ -11,22 +11,26 @@ const userSchema = new Schema( type: String, required: true, }, - companyId: { - type: Schema.Types.ObjectId, - ref: "Company", + role: { + type: String, required: true, }, name: { type: String, required: true, }, - role: { - type: String, - required: true, - }, avatar: { type: String, }, + companyId: { + type: Schema.Types.ObjectId, + ref: "Company", + required: true, + }, + buildIds: { + type: [Schema.Types.ObjectId], + ref: "Build", + }, }, { timestamps: true, diff --git a/server/src/routes/actions.ts b/server/src/routes/actions.ts index 540612b..9a5e527 100644 --- a/server/src/routes/actions.ts +++ b/server/src/routes/actions.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import Action from "../models/Action"; +import Action from "../models/Action.js"; const actionsRouter = Router(); diff --git a/server/src/routes/admin/adminBuildsRoute.ts b/server/src/routes/admin/adminBuildsRoute.ts new file mode 100644 index 0000000..d5d886c --- /dev/null +++ b/server/src/routes/admin/adminBuildsRoute.ts @@ -0,0 +1,60 @@ +import { Router } from "express"; +import Build from "../../models/Build.js"; + +const router = Router(); + +router.get("/", async (req, res) => { + try { + const result = await Build.find(req.query); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const result = await Build.findById(req.params.id); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.post("/", async (req, res) => { + try { + const result = await Build.create(req.body); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.put("/:id", async (req, res) => { + try { + const result = await Build.findByIdAndUpdate(req.params.id, req.body, { + new: true, + }); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.delete("/:id", async (req, res) => { + try { + const result = await Build.findByIdAndRemove(req.params.id); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +const adminBuildsRoute = router; + +export default adminBuildsRoute; diff --git a/server/src/routes/admin/adminCompaniesRoute.ts b/server/src/routes/admin/adminCompaniesRoute.ts new file mode 100644 index 0000000..b6a0208 --- /dev/null +++ b/server/src/routes/admin/adminCompaniesRoute.ts @@ -0,0 +1,60 @@ +import { Router } from "express"; +import Company from "../../models/Company.js"; + +const router = Router(); + +router.get("/", async (req, res) => { + try { + const result = await Company.find(req.query); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const result = await Company.findById(req.params.id); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.post("/", async (req, res) => { + try { + const result = await Company.create(req.body); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.put("/:id", async (req, res) => { + try { + const result = await Company.findByIdAndUpdate(req.params.id, req.body, { + new: true, + }); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.delete("/:id", async (req, res) => { + try { + const result = await Company.findByIdAndRemove(req.params.id); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +const adminCompaniesRoute = router; + +export default adminCompaniesRoute; diff --git a/server/src/routes/admin/adminUsersRoute.ts b/server/src/routes/admin/adminUsersRoute.ts new file mode 100644 index 0000000..cca78e2 --- /dev/null +++ b/server/src/routes/admin/adminUsersRoute.ts @@ -0,0 +1,62 @@ +import bcrypt from "bcrypt"; +import { Router } from "express"; +import User from "../../models/User.js"; + +const router = Router(); + +router.get("/", async (req, res) => { + try { + const result = await User.find(req.query); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.get("/:id", async (req, res) => { + try { + const result = await User.findById(req.params.id); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.post("/", async (req, res) => { + try { + const passwordHash = bcrypt.hashSync(req.body.password, 12); + const result = await User.create({ ...req.body, password: passwordHash }); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.put("/:id", async (req, res) => { + try { + const result = await User.findByIdAndUpdate(req.params.id, req.body, { + new: true, + }); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +router.delete("/:id", async (req, res) => { + try { + const result = await User.findByIdAndRemove(req.params.id); + + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); + +const adminUsersRoute = router; + +export default adminUsersRoute; diff --git a/server/src/routes/builds.ts b/server/src/routes/builds.ts index 5a300c4..cfa0631 100644 --- a/server/src/routes/builds.ts +++ b/server/src/routes/builds.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import Build from "../models/Build"; +import Build from "../models/Build.js"; const buildsRouter = Router(); diff --git a/server/src/routes/companies.ts b/server/src/routes/companies.ts index 92fefea..22497e1 100644 --- a/server/src/routes/companies.ts +++ b/server/src/routes/companies.ts @@ -1,10 +1,9 @@ import { Router } from "express"; -import Company from "../models/Company"; import { parseISO, startOfDay, endOfDay } from "date-fns"; -import ScheduledSession from "../models/ScheduledSession"; -import Schedule from "../models/Schedule"; -import BuildUser from "../models/BuildUser"; -import User from "../models/User"; +import Company from "../models/Company.js"; +import ScheduledSession from "../models/ScheduledSession.js"; +import Schedule from "../models/Schedule.js"; +import User from "../models/User.js"; const router = Router(); @@ -55,8 +54,8 @@ router.get("/:id/builds/:buildId/users", async (req, res) => { return; } - const buildUsers: any = await BuildUser.find({ - buildId: req.params.buildId, + const buildUsers: any = await User.find({ + buildIds: req.params.buildId, }); const users = []; @@ -195,10 +194,8 @@ router.get( return; } - const buildUsers = await BuildUser.find({ buildId: req.params.buildId }); - let buildUserIds = buildUsers.map((buildUser) => - buildUser.userId.toString() - ); + const buildUsers = await User.find({ buildIds: req.params.buildId }); + let buildUserIds = buildUsers.map((buildUser) => buildUser.toString()); const scheduledSessionsAtTime = await ScheduledSession.find({ buildId: req.params.buildId, diff --git a/server/src/routes/login.ts b/server/src/routes/login.ts index 58d0b87..21ed136 100644 --- a/server/src/routes/login.ts +++ b/server/src/routes/login.ts @@ -1,8 +1,8 @@ import { Router } from "express"; import jwt, { Secret } from "jsonwebtoken"; import bcrypt from "bcrypt"; -import User from "../models/User"; -import Token from "../models/Token"; +import User from "../models/User.js"; +import Token from "../models/Token.js"; const router = Router(); diff --git a/server/src/routes/registration.ts b/server/src/routes/registration.ts index 0ea8f84..de33828 100644 --- a/server/src/routes/registration.ts +++ b/server/src/routes/registration.ts @@ -1,8 +1,8 @@ import { Router } from "express"; import jwt, { Secret } from "jsonwebtoken"; import bcrypt from "bcrypt"; -import User from "../models/User"; -import Token from "../models/Token"; +import User from "../models/User.js"; +import Token from "../models/Token.js"; const router = Router(); diff --git a/server/src/routes/scheduledSessions.ts b/server/src/routes/scheduledSessions.ts index 191e897..31b2afe 100644 --- a/server/src/routes/scheduledSessions.ts +++ b/server/src/routes/scheduledSessions.ts @@ -1,7 +1,7 @@ import { Router } from "express"; -import ScheduledSession from "../models/ScheduledSession"; -import Build from "../models/Build"; -import Schedule from "../models/Schedule"; +import ScheduledSession from "../models/ScheduledSession.js"; +import Build from "../models/Build.js"; +import Schedule from "../models/Schedule.js"; import { addMinutes, areIntervalsOverlapping, diff --git a/server/src/routes/schedules.ts b/server/src/routes/schedules.ts index dc8e61e..2b56676 100644 --- a/server/src/routes/schedules.ts +++ b/server/src/routes/schedules.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import Schedule from "../models/Schedule"; +import Schedule from "../models/Schedule.js"; const schedulesRouter = Router(); diff --git a/server/src/routes/users.ts b/server/src/routes/users.ts index e25cf71..3a62862 100644 --- a/server/src/routes/users.ts +++ b/server/src/routes/users.ts @@ -1,21 +1,28 @@ import { Router } from "express"; +import User from "../models/User.js"; // import User from "../models/User.js"; const router = Router(); -// usersRouter.get("/", async (_req, res) => { -// const users = await User.find(); -// console.log(users); +router.get("/", async (req, res) => { + try { + const result = await User.find(req.query); -// res.json(users); -// }); + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); -// usersRouter.get("/:id", async (req, res) => { -// const user = await User.findById(req.params.id); -// console.log(user); +router.get("/:id", async (req, res) => { + try { + const result = await User.findById(req.params); -// res.json(user); -// }); + res.json(result); + } catch (error) { + res.json({ error: (error as Error).message }); + } +}); const usersRouter = router; diff --git a/server/tsconfig.json b/server/tsconfig.json index 2ca8d8c..19bdf09 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,18 +1,109 @@ { "compilerOptions": { - "target": "ESNext", - "module": "CommonJS", - "moduleResolution": "Node", - "outDir": "./dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "skipLibCheck": true, - }, - "ts-node": { - "transpileOnly": true + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "NodeNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ } }