mirror of
https://github.com/rjNemo/ticket_manager
synced 2026-06-12 11:46:40 +00:00
merge latest react branch
This commit is contained in:
commit
5b57423d68
48 changed files with 939 additions and 374 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -7,4 +7,4 @@ app.db*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
app.db
|
app.db
|
||||||
client/node_modules
|
client/node_modules
|
||||||
Scripts/
|
client/src/pages/TestPage.tsx
|
||||||
|
|
@ -9,8 +9,8 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace TicketManager.Controllers
|
namespace TicketManager.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
// [Authorize]
|
||||||
[Route("api/v1/[controller]")]
|
[Route("api/v1/users")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class UsersController : ControllerBase
|
public class UsersController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ namespace TicketManager.DTO
|
||||||
Progression = project.Progression;
|
Progression = project.Progression;
|
||||||
Status = project.Status.ToString();
|
Status = project.Status.ToString();
|
||||||
Manager = project.Manager;
|
Manager = project.Manager;
|
||||||
AppUsers = project.GetMembers();
|
Users = project.GetMembers();
|
||||||
Tickets = project.Tickets;
|
Tickets = project.Tickets;
|
||||||
Activities = project.Activities;
|
Activities = project.Activities;
|
||||||
Files = project.Files;
|
Files = project.Files;
|
||||||
|
|
@ -36,7 +36,7 @@ namespace TicketManager.DTO
|
||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
|
|
||||||
public AppUser Manager { get; set; }
|
public AppUser Manager { get; set; }
|
||||||
public List<AppUser> AppUsers { get; set; } = new List<AppUser>();
|
public List<AppUser> Users { get; set; } = new List<AppUser>();
|
||||||
|
|
||||||
public List<Ticket> Tickets { get; set; } = new List<Ticket>();
|
public List<Ticket> Tickets { get; set; } = new List<Ticket>();
|
||||||
|
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -33,14 +33,17 @@
|
||||||
|
|
||||||
## TO DO
|
## TO DO
|
||||||
|
|
||||||
- Write API tests using Postman: request + test, environment variables, mock server
|
- [ ] Write API tests using Postman: request + test, environment variables, mock server
|
||||||
- Annotate API request in controllers
|
- [ ] Annotate API request in controllers
|
||||||
- Annotate Properties in Models
|
- [ ] Annotate Properties in Models
|
||||||
- Write backend tests
|
- [ ] Write backend tests
|
||||||
- Have a Look at typeahead component
|
- [ ] Have a Look at typeahead component
|
||||||
- Ensure Tickets Edits belong to Project Edits
|
- [ ] Ensure Tickets Edits belong to Project Edits
|
||||||
- Ensure Tickets Files belong to Project Files
|
- [ ] Ensure Tickets Files belong to Project Files
|
||||||
- Async model methods ?
|
- [ ] Async model methods ?
|
||||||
- update assignments automatically from context
|
- [ ] update assignments automatically from context
|
||||||
- use PATCH instead of PUT
|
- [ ] use PATCH instead of PUT
|
||||||
- logging
|
- [ ] logging
|
||||||
|
- [ ] check useRef, useReducer, dispatch
|
||||||
|
- [ ] error page redirect when offline.
|
||||||
|
- [ ] ticket/files/activities list placeholders when empty
|
||||||
|
|
|
||||||
1
Scripts/apiQueries.sh
Executable file
1
Scripts/apiQueries.sh
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
curl --insecure https://localhost:5001/api/v1/
|
||||||
1
Scripts/authentication.sh
Executable file
1
Scripts/authentication.sh
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
.panel {
|
|
||||||
padding-left: 0px;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
.field {
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
.city {
|
|
||||||
display: flex;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(2, 0, 36, 1) 0%,
|
|
||||||
rgba(25, 112, 245, 0.6399510487788865) 0%,
|
|
||||||
rgba(0, 212, 255, 1) 100%
|
|
||||||
);
|
|
||||||
flex-direction: column;
|
|
||||||
height: 40vh;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0px 20px 20px 20px;
|
|
||||||
margin: 0px 0px 50px 0px;
|
|
||||||
border: 1px solid;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 2px 2px #888888;
|
|
||||||
font-family: "Merriweather", serif;
|
|
||||||
}
|
|
||||||
.city h1 {
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.city span {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
.city .row {
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
.weatherError {
|
|
||||||
color: #f16051;
|
|
||||||
font-size: 20px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
font-weight: 200;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { AppRouter } from "./utils/router";
|
|
||||||
import "./App.css";
|
|
||||||
import Layout from "./pages/Layout";
|
import Layout from "./pages/Layout";
|
||||||
|
|
||||||
const App: FC = () => {
|
const App: FC = () => {
|
||||||
|
|
|
||||||
38
client/src/VM/ProjectVM.ts
Normal file
38
client/src/VM/ProjectVM.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Ticket } from "../types/Ticket";
|
||||||
|
import { Project } from "../types/Project";
|
||||||
|
import { AppFile } from "../types/AppFile";
|
||||||
|
import { Activity } from "../types/Activity";
|
||||||
|
import { User } from "../types/User";
|
||||||
|
import { getRemainingdays } from "../utils/methods";
|
||||||
|
|
||||||
|
export default class ProjectVM {
|
||||||
|
public id: number;
|
||||||
|
public title: string;
|
||||||
|
public description: string;
|
||||||
|
public value: number;
|
||||||
|
public tickets: Ticket[];
|
||||||
|
public users: User[];
|
||||||
|
public ticketsTotalCount: number;
|
||||||
|
public ticketsDone: number;
|
||||||
|
public remainingDays: number;
|
||||||
|
public files: AppFile[];
|
||||||
|
public activities: Activity[];
|
||||||
|
|
||||||
|
public constructor(project: Project) {
|
||||||
|
this.id = project.id;
|
||||||
|
this.title = project.title;
|
||||||
|
this.description = project.description;
|
||||||
|
this.users = project.users;
|
||||||
|
this.value = project.progression;
|
||||||
|
this.tickets = project.tickets;
|
||||||
|
this.ticketsTotalCount =
|
||||||
|
this.tickets === undefined ? 0 : this.tickets.length;
|
||||||
|
this.ticketsDone =
|
||||||
|
this.tickets === undefined
|
||||||
|
? 0
|
||||||
|
: this.tickets.filter(t => t.status === "Done").length;
|
||||||
|
this.files = project.files;
|
||||||
|
this.activities = project.activities;
|
||||||
|
this.remainingDays = getRemainingdays(project.plannedEnding);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
client/src/components/ActivityCollection.tsx
Normal file
49
client/src/components/ActivityCollection.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Activity } from "../types/Activity";
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
activities: Activity[];
|
||||||
|
filterText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActivityCollection: FC<IProps> = ({ activities, filterText }) => {
|
||||||
|
return activities === undefined ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ul className="collection">
|
||||||
|
{activities
|
||||||
|
.filter(
|
||||||
|
a =>
|
||||||
|
a.description.toLowerCase().includes(filterText.toLowerCase()) ||
|
||||||
|
a.user.firstName
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(filterText.toLowerCase()) ||
|
||||||
|
a.ticket.title.toLowerCase().includes(filterText.toLowerCase())
|
||||||
|
)
|
||||||
|
.map((activity: Activity) => (
|
||||||
|
<li key={activity.id} className="collection-item avatar">
|
||||||
|
<ActivityEntry activity={activity} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type IFProps = {
|
||||||
|
activity: Activity;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActivityEntry: FC<IFProps> = ({ activity }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img src={activity.user.picture} alt="" className="circle" />
|
||||||
|
{/* <i className="material-icons circle">folder</i> */}
|
||||||
|
<span className="title">
|
||||||
|
{activity.user.firstName} {activity.description} {activity.ticket.title}
|
||||||
|
</span>
|
||||||
|
<p>{activity.date.toDateString()}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
client/src/components/ActivityList.tsx
Normal file
34
client/src/components/ActivityList.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
|
||||||
|
import { ActivityCollection } from "./ActivityCollection";
|
||||||
|
import { Activity } from "../types/Activity";
|
||||||
|
import { FilterBar } from "./FilterBar";
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
activities: Activity[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActivityList: FC<IProps> = ({ activities }) => {
|
||||||
|
const [filterText, setFilterText] = useState<string>("");
|
||||||
|
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
|
||||||
|
setFilterText("");
|
||||||
|
};
|
||||||
|
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
|
||||||
|
e: ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
setFilterText(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="row valign-wrapper">
|
||||||
|
<h3>Activity</h3>
|
||||||
|
<FilterBar
|
||||||
|
filterText={filterText}
|
||||||
|
handleChange={handleChange}
|
||||||
|
clearFilterText={clearFilterText}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ActivityCollection activities={activities} filterText={filterText} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
35
client/src/components/AppFileList.tsx
Normal file
35
client/src/components/AppFileList.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
|
||||||
|
import { AppFile } from "../types/AppFile";
|
||||||
|
import { FileCollection } from "./FileCollection";
|
||||||
|
import { InputFile } from "./InputFile";
|
||||||
|
import { FilterBar } from "./FilterBar";
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
files: AppFile[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FileList: FC<IProps> = ({ files }) => {
|
||||||
|
const [filterText, setFilterText] = useState<string>("");
|
||||||
|
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
|
||||||
|
setFilterText("");
|
||||||
|
};
|
||||||
|
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
|
||||||
|
e: ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
setFilterText(e.target.value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="row valign-wrapper">
|
||||||
|
<h3>Files</h3>
|
||||||
|
<FilterBar
|
||||||
|
filterText={filterText}
|
||||||
|
handleChange={handleChange}
|
||||||
|
clearFilterText={clearFilterText}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<InputFile />
|
||||||
|
<FileCollection files={files} filterText={filterText} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,15 +1,24 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { FloatingButton } from "./FloatingButton";
|
import { User } from "../types/User";
|
||||||
|
|
||||||
interface AvatarListProps {
|
interface AvatarListProps {
|
||||||
avatars: string[];
|
users: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AvatarList: FC<AvatarListProps> = ({ avatars }) => {
|
export const AvatarList: FC<AvatarListProps> = ({ users }) => {
|
||||||
return (
|
return users === undefined ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
{avatars.map((avatar: string) => (
|
{users.map((user: User, i: number) => (
|
||||||
<img className="circle" src={avatar} width="32vh" height="32vh" />
|
<img
|
||||||
|
key={i}
|
||||||
|
className="circle"
|
||||||
|
src={user.picture}
|
||||||
|
width="32vh"
|
||||||
|
height="32vh"
|
||||||
|
alt={user.fullName}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC, Children } from "react";
|
import React, { FC, MouseEvent } from "react";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
|
@ -6,18 +6,20 @@ interface IProps {
|
||||||
shape?: string;
|
shape?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button: FC<IProps> = ({
|
export const Button: FC<IProps> = ({
|
||||||
size = "small",
|
size = "small",
|
||||||
shape = "",
|
shape = "",
|
||||||
color,
|
color,
|
||||||
text,
|
onClick,
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`waves-effect waves-light btn-${size} ${shape} ${color}`}
|
className={`waves-effect waves-light btn-${size} ${shape} ${color}`}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
45
client/src/components/FileCollection.tsx
Normal file
45
client/src/components/FileCollection.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { AppFile } from "../types/AppFile";
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
files: AppFile[];
|
||||||
|
filterText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FileCollection: FC<IProps> = ({ files, filterText }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ul className="collection">
|
||||||
|
{files
|
||||||
|
.filter(
|
||||||
|
f =>
|
||||||
|
f.name.toLowerCase().includes(filterText.toLowerCase()) ||
|
||||||
|
f.format.toLowerCase().includes(filterText.toLowerCase())
|
||||||
|
)
|
||||||
|
.map((file: AppFile) => (
|
||||||
|
<FileEntry file={file} key={file.id} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type IFProps = {
|
||||||
|
file: AppFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FileEntry: FC<IFProps> = ({ file }) => {
|
||||||
|
return (
|
||||||
|
<li className="collection-item avatar">
|
||||||
|
{/* <img src={require("../images/user_1.jpg")} alt="" className="circle" /> */}
|
||||||
|
<i className="material-icons circle">folder</i>
|
||||||
|
<span className="title">{file.name}</span>
|
||||||
|
<p>
|
||||||
|
{file.size}kb {file.format}
|
||||||
|
</p>
|
||||||
|
<a href="#!" className="secondary-content">
|
||||||
|
<i className="material-icons">more_vert</i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
41
client/src/components/FilterBar.tsx
Normal file
41
client/src/components/FilterBar.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React, { FC, ChangeEvent, MouseEvent } from "react";
|
||||||
|
import { useRouteMatch } from "react-router-dom";
|
||||||
|
|
||||||
|
type IProps = {
|
||||||
|
filterText: string;
|
||||||
|
handleChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
clearFilterText: (e: MouseEvent<HTMLInputElement>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterBar: FC<IProps> = ({
|
||||||
|
filterText,
|
||||||
|
handleChange,
|
||||||
|
clearFilterText
|
||||||
|
}) => {
|
||||||
|
const { url } = useRouteMatch();
|
||||||
|
const placeholder: string = url.split("/")[3] || "users";
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="nav-wrapper">
|
||||||
|
<div className="input-field">
|
||||||
|
<input
|
||||||
|
// className="validate"
|
||||||
|
id="filter"
|
||||||
|
type="search"
|
||||||
|
required
|
||||||
|
name="filter"
|
||||||
|
value={filterText}
|
||||||
|
placeholder={`Filter ${placeholder}`}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<label className="label-icon" htmlFor="search">
|
||||||
|
<i className="material-icons">filter_list</i>
|
||||||
|
</label>
|
||||||
|
<i className="material-icons" onClick={clearFilterText}>
|
||||||
|
close
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC, MouseEvent } from "react";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
onClick?: (e: MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FloatingButton: FC<IProps> = ({
|
export const FloatingButton: FC<IProps> = ({
|
||||||
icon = "add",
|
icon = "add",
|
||||||
size = "small",
|
size = "small",
|
||||||
color = "red"
|
color = "red",
|
||||||
|
onClick
|
||||||
}) => {
|
}) => {
|
||||||
const iconComponent = <i className="material-icons left">{icon}</i>;
|
const iconComponent = <i className="material-icons left">{icon}</i>;
|
||||||
return (
|
return (
|
||||||
<Button color={color} size={size} shape="btn-floating">
|
<Button color={color} size={size} shape="btn-floating" onClick={onClick}>
|
||||||
{iconComponent}
|
{iconComponent}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,44 @@
|
||||||
import React, { FC, MouseEvent } from "react";
|
import React, { FC, MouseEvent } from "react";
|
||||||
import { AvatarList } from "./AvatarList";
|
import { Link } from "react-router-dom";
|
||||||
|
import { getRemainingdays } from "../utils/methods";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
title: string;
|
title: string;
|
||||||
tasksTotalCount?: number;
|
remainingDays: string;
|
||||||
tasksDone?: number;
|
|
||||||
remainingDays?: number;
|
|
||||||
avatars: string[];
|
|
||||||
validateTicket: (event: MouseEvent) => void;
|
validateTicket: (event: MouseEvent) => void;
|
||||||
archiveTicket: (event: MouseEvent) => void;
|
archiveTicket: (event: MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HorizontalCard: FC<IProps> = ({
|
export const HorizontalCard: FC<IProps> = ({
|
||||||
title,
|
title,
|
||||||
tasksDone,
|
|
||||||
tasksTotalCount,
|
|
||||||
remainingDays,
|
remainingDays,
|
||||||
avatars,
|
|
||||||
archiveTicket,
|
archiveTicket,
|
||||||
validateTicket
|
validateTicket
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="col s12">
|
<div className="card horizontal">
|
||||||
<div className="card horizontal">
|
<div className="card-stacked">
|
||||||
<div className="card-stacked">
|
<div className="card-content">
|
||||||
<div className="card-content">
|
<div className="row">
|
||||||
<div className="row">
|
<div className="card-title">
|
||||||
<div className="card-title">
|
<h6>
|
||||||
<h6>{title}</h6>
|
<Link to="#">
|
||||||
</div>
|
<b>{title}</b>
|
||||||
<span>Due {remainingDays} days</span>
|
</Link>
|
||||||
{/* <AvatarList avatars={avatars} /> */}
|
</h6>
|
||||||
<div className="right">
|
</div>
|
||||||
{/* <i className=" material-icons">playlist_add_check</i>
|
<span>Due {getRemainingdays(remainingDays)} days</span>
|
||||||
<span>
|
<div className="right">
|
||||||
{" "}
|
<Link to="#">
|
||||||
{tasksDone}/{tasksTotalCount}
|
<i className="material-icons" onClick={validateTicket}>
|
||||||
</span> */}
|
check
|
||||||
|
</i>
|
||||||
<a>
|
</Link>
|
||||||
<i className="material-icons" onClick={validateTicket}>
|
<Link to="#">
|
||||||
check
|
<i className="material-icons" onClick={archiveTicket}>
|
||||||
</i>
|
archive
|
||||||
</a>
|
</i>
|
||||||
<a>
|
</Link>
|
||||||
<i className="material-icons" onClick={archiveTicket}>
|
|
||||||
archive
|
|
||||||
</i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
29
client/src/components/InputFile.tsx
Normal file
29
client/src/components/InputFile.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
type IProps = {};
|
||||||
|
|
||||||
|
export const InputFile: FC<IProps> = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form action="/upload">
|
||||||
|
<div className="file-field input-field">
|
||||||
|
<div className="btn">
|
||||||
|
<i className="material-icons ">cloud_upload</i>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept=".doc,.docx,.pdf,.md,.gdoc,.zip,image/*"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="file-path-wrapper">
|
||||||
|
<input
|
||||||
|
className="file-path validate"
|
||||||
|
type="text"
|
||||||
|
placeholder="Upload one or more files"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
24
client/src/components/Modal.tsx
Normal file
24
client/src/components/Modal.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React, { FC, useState, CSSProperties } from "react";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
handleClose: () => void;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
export const Modal: FC<IProps> = ({ handleClose, show, children }) => {
|
||||||
|
const showHideStyle: CSSProperties = show
|
||||||
|
? { display: "block", zIndex: 10 }
|
||||||
|
: { display: "none", zIndex: 10 };
|
||||||
|
return (
|
||||||
|
<div className="modal" style={showHideStyle}>
|
||||||
|
<div className="modal-content">{children}</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="modal-close waves-effect waves-green btn"
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
55
client/src/components/Preloader.tsx
Normal file
55
client/src/components/Preloader.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
export const Preloader: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="preloader-wrapper big active">
|
||||||
|
<div className="spinner-layer spinner-blue">
|
||||||
|
<div className="circle-clipper left">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
<div className="gap-patch">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
<div className="circle-clipper right">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="spinner-layer spinner-red">
|
||||||
|
<div className="circle-clipper left">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
<div className="gap-patch">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
<div className="circle-clipper right">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="spinner-layer spinner-yellow">
|
||||||
|
<div className="circle-clipper left">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
<div className="gap-patch">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
<div className="circle-clipper right">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="spinner-layer spinner-green">
|
||||||
|
<div className="circle-clipper left">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
<div className="gap-patch">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
<div className="circle-clipper right">
|
||||||
|
<div className="circle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC, HTMLAttributes, CSSProperties } from "react";
|
import React, { FC, CSSProperties } from "react";
|
||||||
|
|
||||||
type ProgressBarProps = {
|
type ProgressBarProps = {
|
||||||
value: number;
|
value: number;
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,47 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { TabRouterHeader } from "./TabRouterHeader";
|
import { TabRouterHeader } from "./TabRouterHeader";
|
||||||
import { TicketList } from "./TicketList";
|
import { TicketList } from "./TicketList";
|
||||||
|
import { FileList } from "./AppFileList";
|
||||||
import { Ticket } from "../types/Ticket";
|
import { Ticket } from "../types/Ticket";
|
||||||
import { Switch, Route, useRouteMatch, Redirect } from "react-router-dom";
|
import { AppFile } from "../types/AppFile";
|
||||||
|
import { Route, useRouteMatch, Redirect } from "react-router-dom";
|
||||||
|
import { ActivityList } from "./ActivityList";
|
||||||
|
import { Activity } from "../types/Activity";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tickets: Ticket[];
|
tickets: Ticket[];
|
||||||
tasksTotalCount?: number;
|
|
||||||
tasksDone?: number;
|
|
||||||
remainingDays?: number;
|
remainingDays?: number;
|
||||||
avatars: string[];
|
tabNames: string[];
|
||||||
|
files: AppFile[];
|
||||||
|
activities: Activity[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabRouter: FC<IProps> = ({
|
export const TabRouter: FC<IProps> = ({
|
||||||
tickets,
|
tickets,
|
||||||
tasksDone,
|
tabNames,
|
||||||
tasksTotalCount,
|
files,
|
||||||
remainingDays,
|
activities
|
||||||
avatars
|
|
||||||
}) => {
|
}) => {
|
||||||
const { url } = useRouteMatch();
|
const { url } = useRouteMatch();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Switch>
|
<div className="row">
|
||||||
<div className="row">
|
<TabRouterHeader tabNames={tabNames} />
|
||||||
<TabRouterHeader />
|
|
||||||
|
|
||||||
<Redirect from={url} to={`${url}/tickets`} />
|
<Redirect from={url} to={`${url}/tickets`} />
|
||||||
|
|
||||||
<Route path={`${url}/tickets`}>
|
<Route path={`${url}/tickets`}>
|
||||||
<TicketList
|
<TicketList tickets={tickets} />
|
||||||
tickets={tickets}
|
</Route>
|
||||||
tasksDone={tasksDone}
|
|
||||||
tasksTotalCount={tasksTotalCount}
|
|
||||||
remainingDays={remainingDays}
|
|
||||||
avatars={avatars}
|
|
||||||
/>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path={`${url}/files`}>
|
<Route path={`${url}/files`}>
|
||||||
{/* <TicketList tickets={tickets} /> */}
|
<FileList files={files} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path={`${url}/activity`}>
|
<Route path={`${url}/activity`}>
|
||||||
{/* <TicketList tickets={tickets} /> */}
|
<ActivityList activities={activities} />
|
||||||
</Route>
|
</Route>
|
||||||
</div>
|
</div>
|
||||||
</Switch>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,50 @@
|
||||||
import React, { FC, useState } from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { Link, useRouteMatch } from "react-router-dom";
|
import { Link, useRouteMatch } from "react-router-dom";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
tabClass?: string;
|
||||||
|
tabNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabRouterHeader: FC<IProps> = ({
|
||||||
|
tabClass = "tab col s4",
|
||||||
|
tabNames
|
||||||
|
}) => {
|
||||||
|
const [isActive, setIsActive] = useState(0);
|
||||||
|
const nTabs = tabNames.length;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ul className="tabs z-depth-1">
|
||||||
|
{tabNames.map((name, i) => (
|
||||||
|
<TabUnit
|
||||||
|
key={i}
|
||||||
|
text={name}
|
||||||
|
value={i.toString()}
|
||||||
|
tabClass={tabClass}
|
||||||
|
isActive={isActive}
|
||||||
|
setIsActive={setIsActive}
|
||||||
|
nTabs={nTabs}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className="indicator"
|
||||||
|
style={{
|
||||||
|
left: `${(isActive / nTabs) * 100}%`,
|
||||||
|
right: `${(1 - (isActive + 1) / nTabs) * 100}%`
|
||||||
|
}}
|
||||||
|
></li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface TabUnitProps {
|
interface TabUnitProps {
|
||||||
tabClass: string;
|
tabClass: string;
|
||||||
isActive: number;
|
isActive: number;
|
||||||
setIsActive: React.Dispatch<React.SetStateAction<number>>;
|
setIsActive: React.Dispatch<React.SetStateAction<number>>;
|
||||||
text: string;
|
text: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
nTabs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TabUnit: FC<TabUnitProps> = ({
|
const TabUnit: FC<TabUnitProps> = ({
|
||||||
|
|
@ -14,15 +52,23 @@ const TabUnit: FC<TabUnitProps> = ({
|
||||||
isActive,
|
isActive,
|
||||||
setIsActive,
|
setIsActive,
|
||||||
text,
|
text,
|
||||||
value
|
value,
|
||||||
|
nTabs
|
||||||
}) => {
|
}) => {
|
||||||
const { url } = useRouteMatch();
|
const { url } = useRouteMatch();
|
||||||
return (
|
return (
|
||||||
<li className={tabClass} key={value}>
|
<li
|
||||||
|
className={tabClass}
|
||||||
|
key={value}
|
||||||
|
style={{
|
||||||
|
left: `${(isActive / nTabs) * 100}%`,
|
||||||
|
right: `${(1 - (isActive + 1) / nTabs) * 100}%`
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
to={`${url}/${text}`}
|
to={`${url}/${text}`}
|
||||||
id={value}
|
id={value}
|
||||||
className={isActive === parseInt(value) ? "active" : ""}
|
className={isActive === parseInt(value) ? "active pink lighten-5" : ""}
|
||||||
onClick={() => setIsActive(parseInt(value))}
|
onClick={() => setIsActive(parseInt(value))}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
|
|
@ -30,50 +76,3 @@ const TabUnit: FC<TabUnitProps> = ({
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
tabClass?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TabRouterHeader: FC<IProps> = ({
|
|
||||||
tabClass = "tab col s3",
|
|
||||||
|
|
||||||
children
|
|
||||||
}) => {
|
|
||||||
const [isActive, setIsActive] = useState(1);
|
|
||||||
|
|
||||||
// const switchTab = (e: React.MouseEvent<HTMLAnchorElement>): void => {
|
|
||||||
// e.preventDefault();
|
|
||||||
// setIsActive(e.target.id);
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="row col s12">
|
|
||||||
<ul className="tabs">
|
|
||||||
<TabUnit
|
|
||||||
text="Tickets"
|
|
||||||
value="1"
|
|
||||||
tabClass={tabClass}
|
|
||||||
isActive={isActive}
|
|
||||||
setIsActive={setIsActive}
|
|
||||||
/>
|
|
||||||
<TabUnit
|
|
||||||
text="Files"
|
|
||||||
value="2"
|
|
||||||
tabClass={tabClass}
|
|
||||||
isActive={isActive}
|
|
||||||
setIsActive={setIsActive}
|
|
||||||
/>
|
|
||||||
<TabUnit
|
|
||||||
text="Activity"
|
|
||||||
value="3"
|
|
||||||
tabClass={tabClass}
|
|
||||||
isActive={isActive}
|
|
||||||
setIsActive={setIsActive}
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,62 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
|
||||||
import { Ticket } from "../types/Ticket";
|
import { Ticket } from "../types/Ticket";
|
||||||
import { FloatingButton } from "./FloatingButton";
|
import { FloatingButton } from "./FloatingButton";
|
||||||
import { HorizontalCard } from "./HorizontalCard";
|
import { HorizontalCard } from "./HorizontalCard";
|
||||||
|
import { FilterBar } from "./FilterBar";
|
||||||
|
|
||||||
type TicketListProps = {
|
type TicketListProps = {
|
||||||
tickets: Ticket[];
|
tickets: Ticket[];
|
||||||
tasksTotalCount?: number;
|
|
||||||
tasksDone?: number;
|
|
||||||
remainingDays?: number;
|
|
||||||
avatars: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TicketList: FC<TicketListProps> = ({
|
export const TicketList: FC<TicketListProps> = ({ tickets }) => {
|
||||||
tickets,
|
const [filterText, setFilterText] = useState<string>("");
|
||||||
tasksDone,
|
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
|
||||||
tasksTotalCount,
|
setFilterText("");
|
||||||
remainingDays,
|
};
|
||||||
avatars
|
|
||||||
}) => {
|
|
||||||
const archiveTicket = () => {};
|
const archiveTicket = () => {};
|
||||||
const validateTicket = () => {};
|
const validateTicket = () => {};
|
||||||
|
const onClick: (e: MouseEvent) => void = (e: MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
|
||||||
|
e: ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
setFilterText(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col s12">
|
<>
|
||||||
<div className="row valign-wrapper">
|
<div className="row valign-wrapper">
|
||||||
<div className="col s6 m4">
|
<h3>Tickets</h3>
|
||||||
<h2>Tickets</h2>
|
<FloatingButton
|
||||||
</div>
|
color=" blue-grey lighten-4"
|
||||||
<div className="col s6 m8">
|
size="small"
|
||||||
<FloatingButton color="grey" size="big" />
|
onClick={onClick}
|
||||||
</div>
|
/>
|
||||||
|
<FilterBar
|
||||||
|
filterText={filterText}
|
||||||
|
handleChange={handleChange}
|
||||||
|
clearFilterText={clearFilterText}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col s12 grey">
|
||||||
<ul>
|
<ul>
|
||||||
{tickets.map((t: Ticket) => (
|
{tickets
|
||||||
<li key={t.id}>
|
.filter(t =>
|
||||||
<HorizontalCard
|
t.title.toLowerCase().includes(filterText.toLowerCase())
|
||||||
title={t.title}
|
)
|
||||||
tasksDone={tasksDone}
|
.map((t: Ticket) => (
|
||||||
tasksTotalCount={tasksTotalCount}
|
<li key={t.id}>
|
||||||
remainingDays={remainingDays}
|
<HorizontalCard
|
||||||
avatars={avatars}
|
title={t.title}
|
||||||
validateTicket={validateTicket}
|
remainingDays={t.plannedEnding}
|
||||||
archiveTicket={archiveTicket}
|
validateTicket={validateTicket}
|
||||||
/>
|
archiveTicket={archiveTicket}
|
||||||
</li>
|
/>
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
103
client/src/components/UsersModal.tsx
Normal file
103
client/src/components/UsersModal.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import React, { FC, useState, ChangeEvent, useEffect } from "react";
|
||||||
|
import { Modal } from "./Modal";
|
||||||
|
import { AvatarList } from "./AvatarList";
|
||||||
|
import { User } from "../types/User";
|
||||||
|
import { FilterBar } from "./FilterBar";
|
||||||
|
import { HttpResponse } from "../types/HttpResponse";
|
||||||
|
import { get } from "../utils/http";
|
||||||
|
import { Constants } from "../utils/Constants";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
show: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
users: User[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UsersModal: FC<IProps> = ({ show, handleClose, users }) => {
|
||||||
|
const [filterText, setFilterText] = useState<string>("");
|
||||||
|
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
|
||||||
|
e: ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
setFilterText(e.target.value);
|
||||||
|
};
|
||||||
|
const [allUsers, setAllUsers] = useState();
|
||||||
|
|
||||||
|
async function httpGet(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response: HttpResponse<User> = await get<User>(
|
||||||
|
`${Constants.usersURI}`
|
||||||
|
);
|
||||||
|
if (response.parsedBody !== undefined) {
|
||||||
|
setAllUsers(response.parsedBody);
|
||||||
|
// setIsLoading(false);
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
// setHasError(true);
|
||||||
|
// setError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if (id !== undefined) {
|
||||||
|
httpGet();
|
||||||
|
// } else {
|
||||||
|
// setHasError(true);
|
||||||
|
// setError("Bad Request");
|
||||||
|
// }
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal show={show} handleClose={handleClose}>
|
||||||
|
<div className="row valign-wrapper blue">
|
||||||
|
<div className="col s10">
|
||||||
|
<h4 className="white-text">Manage users</h4>
|
||||||
|
</div>
|
||||||
|
<div className="col s2">
|
||||||
|
<i
|
||||||
|
className="right material-icons blue lighten-3 circle"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
close
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="center">
|
||||||
|
<AvatarList users={users} />
|
||||||
|
<FilterBar
|
||||||
|
filterText={filterText}
|
||||||
|
clearFilterText={() => setFilterText("")}
|
||||||
|
handleChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* <div className="code">{allUsers}</div> */}
|
||||||
|
<form>
|
||||||
|
<ul>
|
||||||
|
{users.map((u: User) => (
|
||||||
|
<li key={u.id}>
|
||||||
|
<div className="row">
|
||||||
|
<input
|
||||||
|
id={u.id}
|
||||||
|
type="checkbox"
|
||||||
|
name="active"
|
||||||
|
value="true"
|
||||||
|
onChange={() => false}
|
||||||
|
// checked
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{u.fullName}
|
||||||
|
<img
|
||||||
|
className="circle"
|
||||||
|
src={u.picture}
|
||||||
|
width="32vh"
|
||||||
|
height="32vh"
|
||||||
|
alt={u.fullName}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
19
client/src/controllers/ErrorController.tsx
Normal file
19
client/src/controllers/ErrorController.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Redirect } from "react-router-dom";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
error: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorController: FC<IProps> = ({ error }) => {
|
||||||
|
switch (error) {
|
||||||
|
case "Bad Request":
|
||||||
|
return <Redirect to="/400" />;
|
||||||
|
|
||||||
|
case "Not Found":
|
||||||
|
return <Redirect to="/404" />;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <Redirect to="/404" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,13 +0,0 @@
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import "./index.css";
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as serviceWorker from "./serviceWorker";
|
import * as serviceWorker from "./serviceWorker";
|
||||||
|
|
||||||
|
|
|
||||||
10
client/src/pages/NotFoundPage.tsx
Normal file
10
client/src/pages/NotFoundPage.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
interface IProps {}
|
||||||
|
export const NotFoundPage: FC<IProps> = () => {
|
||||||
|
return (
|
||||||
|
<div className="section">
|
||||||
|
<p>error</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,32 +1,50 @@
|
||||||
import React, { FC } from "react";
|
import React, { FC, useState } from "react";
|
||||||
|
import ProjectVM from "../VM/ProjectVM";
|
||||||
import { Header } from "../components/Header";
|
import { Header } from "../components/Header";
|
||||||
import { AvatarList } from "../components/AvatarList";
|
import { AvatarList } from "../components/AvatarList";
|
||||||
import { ProgressBar } from "../components/ProgressBar";
|
import { ProgressBar } from "../components/ProgressBar";
|
||||||
import ProjectVM from "../viewModels/ProjectVM";
|
|
||||||
import { TabRouter } from "../components/TabRouter";
|
import { TabRouter } from "../components/TabRouter";
|
||||||
import { FloatingButton } from "../components/FloatingButton";
|
import { FloatingButton } from "../components/FloatingButton";
|
||||||
|
import { UsersModal } from "../components/UsersModal";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
viewModel: ProjectVM;
|
viewModel: ProjectVM;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectPage: FC<IProps> = ({ viewModel }) => {
|
export const ProjectPage: FC<IProps> = ({ viewModel }) => {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
avatars,
|
users,
|
||||||
value,
|
value,
|
||||||
tickets,
|
tickets,
|
||||||
ticketsDone,
|
ticketsDone,
|
||||||
ticketsTotalCount,
|
ticketsTotalCount,
|
||||||
remainingDays
|
remainingDays,
|
||||||
|
files,
|
||||||
|
activities
|
||||||
} = viewModel;
|
} = viewModel;
|
||||||
|
|
||||||
|
const tabNames: string[] = ["Tickets", "Files", "Activity"];
|
||||||
|
const [showModal, setShowModal] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Header title={title} description={description} />
|
<Header title={title} description={description} />
|
||||||
<div className="row valign-wrapper">
|
<div className="row valign-wrapper">
|
||||||
<AvatarList avatars={avatars} />
|
<AvatarList users={users} />
|
||||||
<FloatingButton icon="add" color="grey" size="small" />
|
<FloatingButton
|
||||||
|
icon="add"
|
||||||
|
color="grey"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
/>
|
||||||
|
<UsersModal
|
||||||
|
show={showModal}
|
||||||
|
users={users}
|
||||||
|
handleClose={() => setShowModal(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
value={value}
|
value={value}
|
||||||
|
|
@ -35,11 +53,10 @@ export const ProjectPage: FC<IProps> = ({ viewModel }) => {
|
||||||
remainingDays={remainingDays}
|
remainingDays={remainingDays}
|
||||||
/>
|
/>
|
||||||
<TabRouter
|
<TabRouter
|
||||||
|
tabNames={tabNames}
|
||||||
tickets={tickets}
|
tickets={tickets}
|
||||||
tasksDone={ticketsDone}
|
files={files}
|
||||||
tasksTotalCount={ticketsTotalCount}
|
activities={activities}
|
||||||
remainingDays={remainingDays}
|
|
||||||
avatars={avatars}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export const TicketPage: FC = () => {
|
||||||
description="Research, ideate and present brand concepts for client consideration"
|
description="Research, ideate and present brand concepts for client consideration"
|
||||||
title="Brand Concept and Design"
|
title="Brand Concept and Design"
|
||||||
/>
|
/>
|
||||||
<AvatarList avatars={["../images/user_1.jpg", "../images/user_2.jpg"]} />
|
{/* <AvatarList users={["../images/user_1.jpg", "../images/user_2.jpg"]} /> */}
|
||||||
<ProgressBar value={60} />
|
<ProgressBar value={60} />
|
||||||
{/* // <TabView>
|
{/* // <TabView>
|
||||||
// <ChildTicket/>
|
// <ChildTicket/>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,10 @@
|
||||||
|
import { User } from "./User";
|
||||||
|
import { Ticket } from "./Ticket";
|
||||||
|
|
||||||
export interface Activity {
|
export interface Activity {
|
||||||
Id: number;
|
id: number;
|
||||||
|
description: string;
|
||||||
|
date: Date;
|
||||||
|
user: User;
|
||||||
|
ticket: Ticket;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
client/src/types/AppFile.ts
Normal file
9
client/src/types/AppFile.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { User } from "./User";
|
||||||
|
|
||||||
|
export interface AppFile {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
format: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export interface File {
|
|
||||||
Id: number;
|
|
||||||
}
|
|
||||||
3
client/src/types/HttpResponse.ts
Normal file
3
client/src/types/HttpResponse.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface HttpResponse<T> extends Response {
|
||||||
|
parsedBody?: T;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Ticket } from "./Ticket";
|
import { Ticket } from "./Ticket";
|
||||||
import { User } from "./User";
|
import { User } from "./User";
|
||||||
|
import { AppFile } from "./AppFile";
|
||||||
import { Activity } from "./Activity";
|
import { Activity } from "./Activity";
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
|
|
@ -13,6 +14,6 @@ export interface Project {
|
||||||
manager: User;
|
manager: User;
|
||||||
users: User[];
|
users: User[];
|
||||||
tickets: Ticket[];
|
tickets: Ticket[];
|
||||||
|
files: AppFile[];
|
||||||
activities: Activity[];
|
activities: Activity[];
|
||||||
files: File[];
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
export interface Ticket {
|
export interface Ticket {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
description: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
plannedEnding: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
picture: File;
|
picture: string;
|
||||||
|
firstName: string;
|
||||||
|
fullName?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
export class Constants {
|
export class Constants {
|
||||||
static getProjectURI: string = "/api/projects";
|
static projectsURI: string = "/api/v1/projects";
|
||||||
|
static ticketsURI: string = "/api/v1/tickets";
|
||||||
|
static usersURI: string = "/api/v1/users";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
client/src/utils/http.ts
Normal file
35
client/src/utils/http.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { HttpResponse } from "../types/HttpResponse";
|
||||||
|
|
||||||
|
export async function http<T>(request: RequestInfo): Promise<HttpResponse<T>> {
|
||||||
|
const response: HttpResponse<T> = await fetch(request);
|
||||||
|
try {
|
||||||
|
response.parsedBody = await response.json();
|
||||||
|
} catch (ex) {}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw response.statusText;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get<T>(
|
||||||
|
path: string,
|
||||||
|
args: RequestInit = { method: "get" }
|
||||||
|
): Promise<HttpResponse<T>> {
|
||||||
|
return await http<T>(new Request(path, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function post<T>(
|
||||||
|
path: string,
|
||||||
|
body: any,
|
||||||
|
args: RequestInit = { method: "post", body: JSON.stringify(body) }
|
||||||
|
): Promise<HttpResponse<T>> {
|
||||||
|
return await http<T>(new Request(path, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function put<T>(
|
||||||
|
path: string,
|
||||||
|
body: any,
|
||||||
|
args: RequestInit = { method: "put", body: JSON.stringify(body) }
|
||||||
|
): Promise<HttpResponse<T>> {
|
||||||
|
return await http<T>(new Request(path, args));
|
||||||
|
}
|
||||||
7
client/src/utils/methods.ts
Normal file
7
client/src/utils/methods.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const getRemainingdays: (endDate: string) => number = (
|
||||||
|
endDate: string
|
||||||
|
) => {
|
||||||
|
let endingDate: Date = new Date(endDate);
|
||||||
|
let today: Date = new Date();
|
||||||
|
return Math.abs(endingDate.getDate() - today.getDate());
|
||||||
|
};
|
||||||
|
|
@ -1,19 +1,31 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Router, Route, Switch, Link, NavLink } from "react-router-dom";
|
import {
|
||||||
|
Router,
|
||||||
|
Route,
|
||||||
|
Switch
|
||||||
|
// Redirect
|
||||||
|
//Link, NavLink
|
||||||
|
} from "react-router-dom";
|
||||||
import * as creacteHistory from "history";
|
import * as creacteHistory from "history";
|
||||||
import { TicketPage } from "../pages/TicketPage";
|
// import { TicketPage } from "../pages/TicketPage";
|
||||||
import { HomeController } from "../controllers/HomeController";
|
// import { HomeController } from "../controllers/HomeController";
|
||||||
import { ProjectController } from "../controllers/ProjectController";
|
import { ProjectController } from "../controllers/ProjectController";
|
||||||
import { UserController } from "../controllers/UserController";
|
import { NotFoundPage } from "../pages/NotFoundPage";
|
||||||
import { TicketController } from "../controllers/TicketController";
|
import { TestPage } from "../pages/TestPage";
|
||||||
|
// import { UserController } from "../controllers/UserController";
|
||||||
|
// import { TicketController } from "../controllers/TicketController";
|
||||||
|
|
||||||
export const history = creacteHistory.createBrowserHistory();
|
export const history = creacteHistory.createBrowserHistory();
|
||||||
|
|
||||||
export const AppRouter = () => {
|
export const AppRouter = () => {
|
||||||
return (
|
return (
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<div>
|
<div className="grey lighten-4">
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Route exact path="/">
|
||||||
|
<TestPage />
|
||||||
|
</Route>
|
||||||
|
|
||||||
{/* <Route path="/">
|
{/* <Route path="/">
|
||||||
<HomeController />
|
<HomeController />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
@ -26,6 +38,14 @@ export const AppRouter = () => {
|
||||||
{/* <Route path="/tickets/:id">
|
{/* <Route path="/tickets/:id">
|
||||||
<TicketController />
|
<TicketController />
|
||||||
</Route> */}
|
</Route> */}
|
||||||
|
|
||||||
|
<Route path="/404">
|
||||||
|
<NotFoundPage />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* <Route path="*">
|
||||||
|
<Redirect to="/error" />
|
||||||
|
</Route> */}
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { Ticket } from "../types/Ticket";
|
|
||||||
import { Project } from "../types/Project";
|
|
||||||
import { Constants } from "../utils/Constants";
|
|
||||||
import { User } from "../types/User";
|
|
||||||
|
|
||||||
export default class ProjectVM {
|
|
||||||
public id: number;
|
|
||||||
public title: string;
|
|
||||||
public description: string;
|
|
||||||
public value: number;
|
|
||||||
public tickets: Ticket[];
|
|
||||||
public avatars: string[];
|
|
||||||
public ticketsTotalCount: number;
|
|
||||||
public ticketsDone: number;
|
|
||||||
public remainingDays: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getMembers
|
|
||||||
*/
|
|
||||||
// public getMembers(): string {
|
|
||||||
// let res: Promise<Response> = fetch(
|
|
||||||
// `${Constants.getProjectURI}/${this.id}/members`
|
|
||||||
// );
|
|
||||||
// return JSON.stringify(res);
|
|
||||||
// // res.json();
|
|
||||||
// }
|
|
||||||
|
|
||||||
public constructor(project: Project) {
|
|
||||||
this.id = project.id;
|
|
||||||
this.title = project.title;
|
|
||||||
this.description = project.description;
|
|
||||||
this.avatars = project.users.map(u => u.picture);
|
|
||||||
this.value = project.progression;
|
|
||||||
this.tickets = project.tickets;
|
|
||||||
this.ticketsTotalCount = this.tickets.length;
|
|
||||||
this.ticketsDone = this.tickets.filter(t => t.status === "Done").length;
|
|
||||||
|
|
||||||
let endingDate: Date = new Date(project.plannedEnding);
|
|
||||||
let today: Date = new Date();
|
|
||||||
let plannedEnding: number = Math.abs(
|
|
||||||
endingDate.getDate() - today.getDate()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.remainingDays = plannedEnding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue