mirror of
https://github.com/rjNemo/ticket_manager
synced 2026-06-06 00:36:39 +00:00
pulled all updates
This commit is contained in:
commit
0e6ccd8d62
29 changed files with 719 additions and 121 deletions
|
|
@ -92,7 +92,7 @@ namespace TicketManager.Controllers
|
|||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest();
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
var ticket = new Ticket()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
### v1
|
||||
|
||||
- [Internal Link. Don't forget to update](https://localhost:5001/swagger)
|
||||
- [Internal Link. Don't forget to update](https://localhost:5001/api/v1/)
|
||||
|
||||
## Features
|
||||
|
||||
|
|
@ -45,7 +45,8 @@
|
|||
- [ ] logging
|
||||
- [ ] check useRef, useReducer, dispatch
|
||||
- [ ] error page redirect when offline.
|
||||
- [ ] ticket/files/activities list placeholders when empty
|
||||
- [x] ticket/files/activities list placeholders when empty
|
||||
- [ ] think about public/private DTO's constructor, getters and setters
|
||||
- [<span style="color:red">X</span>] write dtos without circular dependencies.
|
||||
- [ ] use dtoRequest for PutProjects
|
||||
- [ ] render avatarlist after UserModal Update
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ namespace TicketManager
|
|||
app.UseDefaultFiles();
|
||||
|
||||
app.UseSwagger();
|
||||
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
c.RoutePrefix = "api/v1";
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { User } from "../types/User";
|
|||
import { getRemainingdays } from "../utils/methods";
|
||||
|
||||
export default class ProjectVM {
|
||||
public id: number;
|
||||
// public id: number;
|
||||
public title: string;
|
||||
public description: string;
|
||||
public creationDate: string;
|
||||
|
|
@ -24,7 +24,7 @@ export default class ProjectVM {
|
|||
public remainingDays: number;
|
||||
|
||||
public constructor(project: Project, allUsers: User[]) {
|
||||
this.id = project.id;
|
||||
// this.id = project.id;
|
||||
this.title = project.title;
|
||||
this.description = project.description;
|
||||
this.creationDate = project.creationDate;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,19 @@
|
|||
export class UserVM {
|
||||
public Id?: number;
|
||||
import { Project } from "../types/Project";
|
||||
import { Ticket } from "../types/Ticket";
|
||||
import { User } from "../types/User";
|
||||
|
||||
public constructor() {}
|
||||
export class UserVM {
|
||||
public fullName: string;
|
||||
public presentation: string;
|
||||
public picture: string;
|
||||
public projects: Project[];
|
||||
public tickets: Ticket[];
|
||||
|
||||
public constructor(user: User) {
|
||||
this.fullName = user.fullName;
|
||||
this.presentation = user.presentation;
|
||||
this.picture = user.picture;
|
||||
this.projects = user.projects;
|
||||
this.tickets = user.tickets;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,38 +12,58 @@ export const ActivityCollection: FC<IProps> = ({ activities, filterText }) => {
|
|||
) : (
|
||||
<>
|
||||
<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>
|
||||
))}
|
||||
{activities.length === 0 ? (
|
||||
<ActivityEntry />
|
||||
) : (
|
||||
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) => (
|
||||
<ActivityEntry activity={activity} key={activity.id} />
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type IFProps = {
|
||||
activity: Activity;
|
||||
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>
|
||||
<li className="collection-item avatar">
|
||||
{/* <img
|
||||
src={
|
||||
activity
|
||||
? activity.user.picture
|
||||
: "https://previews.123rf.com/images/vikpit/vikpit1604/vikpit160400034/54976526-welcome-sign-symbol-word-welcome-hand-lettering-calligraphic-font-letters-and-shade-isolated-on-whit.jpg"
|
||||
}
|
||||
alt=""
|
||||
height="32vh"
|
||||
width="32vh"
|
||||
className="circle"
|
||||
/> */}
|
||||
<i className="material-icons circle indigo lighten-1">folder</i>
|
||||
<span className="title">
|
||||
{activity ? activity.user.firstName : "Ruidy"}
|
||||
{activity ? activity.description : " welcomes you "}
|
||||
{activity ? activity.ticket.title : "here"}
|
||||
</span>
|
||||
<p>
|
||||
{activity ? activity.date.toDateString() : new Date().toDateString()}
|
||||
</p>
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
12
client/src/components/Avatar.tsx
Normal file
12
client/src/components/Avatar.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React, { FC } from "react";
|
||||
|
||||
interface IProps {
|
||||
picture: string;
|
||||
}
|
||||
export const Avatar: FC<IProps> = ({ picture }) => {
|
||||
return (
|
||||
<>
|
||||
<img className="circle" src={picture} height="100vh" width="100vh" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,35 +7,38 @@ type IProps = {
|
|||
};
|
||||
|
||||
export const FileCollection: FC<IProps> = ({ files, filterText }) => {
|
||||
console.log();
|
||||
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} />
|
||||
))}
|
||||
{files.length === 0 ? (
|
||||
<FileEntry />
|
||||
) : (
|
||||
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;
|
||||
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>
|
||||
<i className="material-icons circle indigo lighten-1">folder</i>
|
||||
<span className="title">{file ? file.name : "Add your first file"}</span>
|
||||
<p>
|
||||
{file.size}kb {file.format}
|
||||
{file ? file.size : 0}kb {file ? file.format : "pdf"}
|
||||
</p>
|
||||
<a href="#!" className="secondary-content">
|
||||
<i className="material-icons">more_vert</i>
|
||||
|
|
|
|||
|
|
@ -3,46 +3,58 @@ import { Link } from "react-router-dom";
|
|||
import { getRemainingdays } from "../utils/methods";
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
remainingDays: string;
|
||||
validateTicket: (event: MouseEvent) => void;
|
||||
archiveTicket: (event: MouseEvent) => void;
|
||||
title?: string;
|
||||
remainingDays?: string;
|
||||
validateTicket?: (event: MouseEvent) => void;
|
||||
// archiveTicket: (event: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export const HorizontalCard: FC<IProps> = ({
|
||||
title,
|
||||
remainingDays,
|
||||
archiveTicket,
|
||||
// archiveTicket,
|
||||
validateTicket
|
||||
}) => {
|
||||
return (
|
||||
<div className="card horizontal">
|
||||
<div className="card-stacked">
|
||||
<div className="card-content">
|
||||
<div className="row">
|
||||
<div className="card-title">
|
||||
<h6>
|
||||
<li>
|
||||
<div className="card horizontal">
|
||||
<div className="card-stacked">
|
||||
<div className="card-content">
|
||||
<div className="row">
|
||||
<div className="card-title">
|
||||
<h6>
|
||||
<Link to="#">
|
||||
<b>{title ?? "Nothing to do"}</b>
|
||||
</Link>
|
||||
</h6>
|
||||
</div>
|
||||
<span>
|
||||
Due{" "}
|
||||
{remainingDays ? (
|
||||
getRemainingdays(remainingDays)
|
||||
) : (
|
||||
<span>
|
||||
<del>Too much</del> 0
|
||||
</span>
|
||||
)}{" "}
|
||||
days
|
||||
</span>
|
||||
<div className="right">
|
||||
<Link to="#">
|
||||
<b>{title}</b>
|
||||
<i className="material-icons" onClick={validateTicket}>
|
||||
check
|
||||
</i>
|
||||
</Link>
|
||||
</h6>
|
||||
</div>
|
||||
<span>Due {getRemainingdays(remainingDays)} days</span>
|
||||
<div className="right">
|
||||
<Link to="#">
|
||||
<i className="material-icons" onClick={validateTicket}>
|
||||
check
|
||||
</i>
|
||||
</Link>
|
||||
<Link to="#">
|
||||
{/* <Link to="#">
|
||||
<i className="material-icons" onClick={archiveTicket}>
|
||||
archive
|
||||
</i>
|
||||
</Link>
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const InputFile: FC<IProps> = () => {
|
|||
<>
|
||||
<form action="/upload">
|
||||
<div className="file-field input-field">
|
||||
<div className="btn">
|
||||
<div className="btn indigo lighten-1">
|
||||
<i className="material-icons ">cloud_upload</i>
|
||||
<input
|
||||
type="file"
|
||||
|
|
|
|||
41
client/src/components/MemberList.tsx
Normal file
41
client/src/components/MemberList.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
|
||||
import { UsersModalEntry } from "./UsersModalEntry";
|
||||
import { FilterBar } from "./FilterBar";
|
||||
import { User } from "../types/User";
|
||||
|
||||
interface IProps {
|
||||
users: User[];
|
||||
}
|
||||
|
||||
export const MemberList: FC<IProps> = ({ users }) => {
|
||||
const [members, setMembers] = useState<User[]>([]);
|
||||
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 (
|
||||
<>
|
||||
<FilterBar
|
||||
filterText={filterText}
|
||||
clearFilterText={clearFilterText}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<ul>
|
||||
{users.map((u: User) => (
|
||||
<li key={u.id}>
|
||||
<UsersModalEntry
|
||||
user={u}
|
||||
members={members}
|
||||
setMembers={setMembers}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
77
client/src/components/NewTicketForm.tsx
Normal file
77
client/src/components/NewTicketForm.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React, { FC } from "react";
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||
description: string;
|
||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||
endingDate: string;
|
||||
setEndingDate: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const NewTicketForm: FC<IProps> = ({
|
||||
title,
|
||||
setTitle,
|
||||
description,
|
||||
setDescription,
|
||||
endingDate,
|
||||
setEndingDate
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="input-field">
|
||||
<i className="material-icons prefix">note_add</i>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
className="validate"
|
||||
value={title}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setTitle(e.target.value)
|
||||
}
|
||||
/>
|
||||
<label htmlFor="title">Title</label>
|
||||
</div>
|
||||
|
||||
<div className="input-field">
|
||||
<i className="material-icons prefix">mode_edit</i>
|
||||
<textarea
|
||||
id="description"
|
||||
className="materialize-textarea validate"
|
||||
value={description}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setDescription(e.target.value)
|
||||
}
|
||||
></textarea>
|
||||
<label htmlFor="description">Description</label>
|
||||
</div>
|
||||
|
||||
<div className="input-field">
|
||||
<i className="material-icons prefix">date_range</i>
|
||||
<input
|
||||
id="Due Date"
|
||||
type="text"
|
||||
className="datepicker"
|
||||
value={endingDate}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setEndingDate(e.target.value)
|
||||
}
|
||||
/>
|
||||
<label htmlFor="Due Date">Due Date</label>
|
||||
</div>
|
||||
|
||||
<div className="input-field">
|
||||
<select id="project" className="browser-default">
|
||||
<option value="" disabled selected>
|
||||
Project
|
||||
</option>
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="3">Option 3</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
93
client/src/components/NewTicketModal.tsx
Normal file
93
client/src/components/NewTicketModal.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import React, { FC, useState, ChangeEvent, useEffect, FormEvent } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Modal } from "./Modal";
|
||||
import { NewTicketTabRouter } from "./NewTicketTabRouter";
|
||||
import { User } from "../types/User";
|
||||
import { Ticket } from "../types/Ticket";
|
||||
import { patch, post } from "../utils/http";
|
||||
import { Constants } from "../utils/Constants";
|
||||
import { Project } from "../types/Project";
|
||||
import { HttpResponse } from "../types/HttpResponse";
|
||||
|
||||
interface IProps {
|
||||
show: boolean;
|
||||
handleClose(): void;
|
||||
allUsers: User[];
|
||||
}
|
||||
|
||||
export const NewTicketModal: FC<IProps> = ({ show, handleClose, allUsers }) => {
|
||||
const [filterText, setFilterText] = useState<string>("");
|
||||
const { id } = useParams();
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [endingDate, setEndingDate] = useState("");
|
||||
|
||||
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setFilterText(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit: (event: FormEvent<HTMLFormElement>) => void = async (
|
||||
e: FormEvent
|
||||
) => {
|
||||
e.preventDefault();
|
||||
let newTicket = {
|
||||
title: title,
|
||||
description: description,
|
||||
endingDate: endingDate,
|
||||
creatorId: "20bf4b2a-7209-4826-96cd-29c2bc937a94",
|
||||
projectId: 1
|
||||
};
|
||||
console.log(newTicket);
|
||||
const response: HttpResponse<Ticket> = await post<Ticket>(
|
||||
`${Constants.ticketsURI}`,
|
||||
newTicket
|
||||
);
|
||||
console.log(response.parsedBody);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
useEffect(() => {});
|
||||
return (
|
||||
<Modal show={show} handleClose={handleClose}>
|
||||
<div className="row valign-wrapper indigo">
|
||||
<div className="col s10">
|
||||
<h4 className="white-text">New Ticket</h4>
|
||||
</div>
|
||||
|
||||
<div className="col s2">
|
||||
<i
|
||||
className="right material-icons indigo lighten-3 circle"
|
||||
onClick={handleClose}
|
||||
>
|
||||
close
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="row">
|
||||
<NewTicketTabRouter
|
||||
tabNames={["Details", "Members"]}
|
||||
users={allUsers}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
endingDate={endingDate}
|
||||
setEndingDate={setEndingDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer grey lighten-3">
|
||||
<input
|
||||
type="submit"
|
||||
className="modal-close waves-effect waves-green btn indigo"
|
||||
value="Create Task"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
55
client/src/components/NewTicketTabRouter.tsx
Normal file
55
client/src/components/NewTicketTabRouter.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import React, { FC } from "react";
|
||||
import { Route, useRouteMatch, Redirect } from "react-router-dom";
|
||||
import { TabRouterHeader } from "./TabRouterHeader";
|
||||
import { NewTicketForm } from "./NewTicketForm";
|
||||
import { MemberList } from "./MemberList";
|
||||
import { User } from "../types/User";
|
||||
import { Ticket } from "../types/Ticket";
|
||||
|
||||
interface IProps {
|
||||
tabNames: string[];
|
||||
users: User[];
|
||||
description: string;
|
||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||
title: string;
|
||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||
endingDate: string;
|
||||
setEndingDate: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const NewTicketTabRouter: FC<IProps> = ({
|
||||
tabNames,
|
||||
users,
|
||||
description,
|
||||
setDescription,
|
||||
title,
|
||||
setTitle,
|
||||
endingDate,
|
||||
setEndingDate
|
||||
}) => {
|
||||
const { url } = useRouteMatch();
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<TabRouterHeader tabNames={tabNames} />
|
||||
|
||||
<Redirect from={url} to={`${url}/details`} />
|
||||
|
||||
<Route path={`${url}/details`}>
|
||||
<NewTicketForm
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
endingDate={endingDate}
|
||||
setEndingDate={setEndingDate}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/members`}>
|
||||
<MemberList users={users} />
|
||||
</Route>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -16,11 +16,12 @@ export const ProgressBar: FC<ProgressBarProps> = ({
|
|||
remainingDays
|
||||
}) => {
|
||||
const styleString: CSSProperties = { width: `${value}%` };
|
||||
const barColor: string = value < 75 ? "red" : "";
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="progress">
|
||||
<div className="determinate" style={styleString}></div>
|
||||
<div className={`determinate ${barColor}`} style={styleString}></div>
|
||||
</div>
|
||||
<div>
|
||||
<i className="left material-icons">playlist_add_check</i>
|
||||
|
|
|
|||
78
client/src/components/ProjectList.tsx
Normal file
78
client/src/components/ProjectList.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import React, { FC, useState, ChangeEvent, MouseEvent } from "react";
|
||||
import { Ticket } from "../types/Ticket";
|
||||
import { FloatingButton } from "./FloatingButton";
|
||||
import { HorizontalCard } from "./HorizontalCard";
|
||||
import { FilterBar } from "./FilterBar";
|
||||
import { put } from "../utils/http";
|
||||
import { Constants } from "../utils/Constants";
|
||||
import { HttpResponse } from "../types/HttpResponse";
|
||||
import { Project } from "../types/Project";
|
||||
|
||||
type IProps = {
|
||||
projects: Project[];
|
||||
};
|
||||
|
||||
export const ProjectList: FC<IProps> = ({ projects }) => {
|
||||
const [filterText, setFilterText] = useState<string>("");
|
||||
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
|
||||
setFilterText("");
|
||||
};
|
||||
// const archiveTicket = () => {};
|
||||
|
||||
const onClick: (e: MouseEvent) => void = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setShowNew(true);
|
||||
};
|
||||
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setFilterText(e.target.value);
|
||||
};
|
||||
|
||||
const [showNew, setShowNew] = useState(false);
|
||||
let filteredTickets = projects.filter(
|
||||
t =>
|
||||
t.status !== "Done" &&
|
||||
t.title.toLowerCase().includes(filterText.toLowerCase())
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="row valign-wrapper">
|
||||
<h3>Projects</h3>
|
||||
<FloatingButton
|
||||
color="indigo lighten-1"
|
||||
size="small"
|
||||
onClick={onClick}
|
||||
/>
|
||||
<FilterBar
|
||||
filterText={filterText}
|
||||
handleChange={handleChange}
|
||||
clearFilterText={clearFilterText}
|
||||
/>
|
||||
</div>
|
||||
<div className="col s12 grey">
|
||||
<ul>
|
||||
{filteredTickets.length === 0 ? (
|
||||
<HorizontalCard />
|
||||
) : (
|
||||
filteredTickets.map((t: Project) => (
|
||||
<HorizontalCard
|
||||
key={t.id}
|
||||
title={t.title}
|
||||
remainingDays={t.endingDate}
|
||||
validateTicket={async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
await put<HttpResponse<Ticket>>(
|
||||
`${Constants.ticketsURI}/${t.id}/closed`,
|
||||
{}
|
||||
);
|
||||
}}
|
||||
// archiveTicket={archiveTicket}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import React, { FC } from "react";
|
||||
import { Route, useRouteMatch, Redirect } from "react-router-dom";
|
||||
import { TabRouterHeader } from "./TabRouterHeader";
|
||||
import { TicketList } from "./TicketList";
|
||||
import { FileList } from "./AppFileList";
|
||||
import { ActivityList } from "./ActivityList";
|
||||
import { Ticket } from "../types/Ticket";
|
||||
import { AppFile } from "../types/AppFile";
|
||||
import { Route, useRouteMatch, Redirect } from "react-router-dom";
|
||||
import { ActivityList } from "./ActivityList";
|
||||
import { Activity } from "../types/Activity";
|
||||
import { User } from "../types/User";
|
||||
|
||||
interface IProps {
|
||||
tickets: Ticket[];
|
||||
|
|
@ -14,15 +15,18 @@ interface IProps {
|
|||
tabNames: string[];
|
||||
files: AppFile[];
|
||||
activities: Activity[];
|
||||
allUsers: User[];
|
||||
}
|
||||
|
||||
export const TabRouter: FC<IProps> = ({
|
||||
tickets,
|
||||
tabNames,
|
||||
files,
|
||||
activities
|
||||
activities,
|
||||
allUsers
|
||||
}) => {
|
||||
const { url } = useRouteMatch();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
|
|
@ -31,7 +35,7 @@ export const TabRouter: FC<IProps> = ({
|
|||
<Redirect from={url} to={`${url}/tickets`} />
|
||||
|
||||
<Route path={`${url}/tickets`}>
|
||||
<TicketList tickets={tickets} />
|
||||
<TicketList tickets={tickets} users={allUsers} />
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/files`}>
|
||||
|
|
|
|||
|
|
@ -7,14 +7,15 @@ interface IProps {
|
|||
}
|
||||
|
||||
export const TabRouterHeader: FC<IProps> = ({
|
||||
tabClass = "tab col s4",
|
||||
tabNames
|
||||
tabNames,
|
||||
tabClass = `tab col s${12 / tabNames.length}`
|
||||
}) => {
|
||||
const [isActive, setIsActive] = useState(0);
|
||||
const nTabs = tabNames.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className="tabs z-depth-1">
|
||||
<ul className="tabs">
|
||||
{tabNames.map((name, i) => (
|
||||
<TabUnit
|
||||
key={i}
|
||||
|
|
@ -27,7 +28,7 @@ export const TabRouterHeader: FC<IProps> = ({
|
|||
/>
|
||||
))}
|
||||
<li
|
||||
className="indicator"
|
||||
className="indicator indigo lighten-2"
|
||||
style={{
|
||||
left: `${(isActive / nTabs) * 100}%`,
|
||||
right: `${(1 - (isActive + 1) / nTabs) * 100}%`
|
||||
|
|
@ -68,7 +69,11 @@ const TabUnit: FC<TabUnitProps> = ({
|
|||
<Link
|
||||
to={`${url}/${text}`}
|
||||
id={value}
|
||||
className={isActive === parseInt(value) ? "active pink lighten-5" : ""}
|
||||
className={
|
||||
isActive === parseInt(value)
|
||||
? "active indigo lighten-5 indigo-text"
|
||||
: "indigo-text"
|
||||
}
|
||||
onClick={() => setIsActive(parseInt(value))}
|
||||
>
|
||||
{text}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,27 @@ import { Ticket } from "../types/Ticket";
|
|||
import { FloatingButton } from "./FloatingButton";
|
||||
import { HorizontalCard } from "./HorizontalCard";
|
||||
import { FilterBar } from "./FilterBar";
|
||||
import { User } from "../types/User";
|
||||
import { HttpResponse } from "../types/HttpResponse";
|
||||
import { put } from "../utils/http";
|
||||
import { Constants } from "../utils/Constants";
|
||||
import { NewTicketModal } from "./NewTicketModal";
|
||||
|
||||
type TicketListProps = {
|
||||
tickets: Ticket[];
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export const TicketList: FC<TicketListProps> = ({ tickets }) => {
|
||||
export const TicketList: FC<TicketListProps> = ({ tickets, users }) => {
|
||||
const [filterText, setFilterText] = useState<string>("");
|
||||
const clearFilterText: (e: MouseEvent) => void = (e: MouseEvent) => {
|
||||
setFilterText("");
|
||||
};
|
||||
const archiveTicket = () => {};
|
||||
const validateTicket = () => {};
|
||||
// const archiveTicket = () => {};
|
||||
|
||||
const onClick: (e: MouseEvent) => void = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setShowNew(true);
|
||||
};
|
||||
const handleChange: (e: ChangeEvent<HTMLInputElement>) => void = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
|
|
@ -24,12 +31,25 @@ export const TicketList: FC<TicketListProps> = ({ tickets }) => {
|
|||
setFilterText(e.target.value);
|
||||
};
|
||||
|
||||
const [showNew, setShowNew] = useState(false);
|
||||
let filteredTickets = tickets.filter(
|
||||
t =>
|
||||
t.status !== "Done" &&
|
||||
t.title.toLowerCase().includes(filterText.toLowerCase())
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="row valign-wrapper">
|
||||
<NewTicketModal
|
||||
handleClose={() => {
|
||||
setShowNew(false);
|
||||
}}
|
||||
show={showNew}
|
||||
allUsers={users}
|
||||
/>
|
||||
<h3>Tickets</h3>
|
||||
<FloatingButton
|
||||
color=" blue-grey lighten-4"
|
||||
color="indigo lighten-3"
|
||||
size="small"
|
||||
onClick={onClick}
|
||||
/>
|
||||
|
|
@ -41,20 +61,25 @@ export const TicketList: FC<TicketListProps> = ({ tickets }) => {
|
|||
</div>
|
||||
<div className="col s12 grey">
|
||||
<ul>
|
||||
{tickets
|
||||
.filter(t =>
|
||||
t.title.toLowerCase().includes(filterText.toLowerCase())
|
||||
)
|
||||
.map((t: Ticket) => (
|
||||
<li key={t.id}>
|
||||
<HorizontalCard
|
||||
title={t.title}
|
||||
remainingDays={t.plannedEnding}
|
||||
validateTicket={validateTicket}
|
||||
archiveTicket={archiveTicket}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{filteredTickets.length === 0 ? (
|
||||
<HorizontalCard />
|
||||
) : (
|
||||
filteredTickets.map((t: Ticket) => (
|
||||
<HorizontalCard
|
||||
key={t.id}
|
||||
title={t.title}
|
||||
remainingDays={t.endingDate}
|
||||
validateTicket={async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
await put<HttpResponse<Ticket>>(
|
||||
`${Constants.ticketsURI}/${t.id}/closed`,
|
||||
{}
|
||||
);
|
||||
}}
|
||||
// archiveTicket={archiveTicket}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
21
client/src/components/UserHeader.tsx
Normal file
21
client/src/components/UserHeader.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React, { FC } from "react";
|
||||
import { Header } from "../components/Header";
|
||||
import { Avatar } from "../components/Avatar";
|
||||
|
||||
interface IProps {
|
||||
fullName: string;
|
||||
presentation: string;
|
||||
picture: string;
|
||||
}
|
||||
export const UserHeader: FC<IProps> = ({ fullName, presentation, picture }) => {
|
||||
return (
|
||||
<div className="row valign-wrapper">
|
||||
<div className="col s2">
|
||||
<Avatar picture={picture} />
|
||||
</div>
|
||||
<div className="col s10">
|
||||
<Header title={fullName} description={presentation} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
34
client/src/components/UserTabRouter.tsx
Normal file
34
client/src/components/UserTabRouter.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React, { FC } from "react";
|
||||
import { Route, useRouteMatch, Redirect } from "react-router-dom";
|
||||
import { TabRouterHeader } from "./TabRouterHeader";
|
||||
import { ProjectList } from "./ProjectList";
|
||||
import { Ticket } from "../types/Ticket";
|
||||
import { Project } from "../types/Project";
|
||||
|
||||
interface IProps {
|
||||
tabNames: string[];
|
||||
tickets: Ticket[];
|
||||
projects: Project[];
|
||||
}
|
||||
|
||||
export const UserTabRouter: FC<IProps> = ({ tickets, tabNames, projects }) => {
|
||||
const { url } = useRouteMatch();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row">
|
||||
<TabRouterHeader tabNames={tabNames} />
|
||||
|
||||
<Redirect from={url} to={`${url}/projects`} />
|
||||
|
||||
<Route path={`${url}/projects`}>
|
||||
<ProjectList projects={projects} />
|
||||
</Route>
|
||||
|
||||
{/* <Route path={`${url}/tickets`}>
|
||||
<TicketList tickets={tickets} />
|
||||
</Route> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -35,22 +35,22 @@ export const UsersModal: FC<IProps> = ({
|
|||
e: FormEvent
|
||||
) => {
|
||||
e.preventDefault();
|
||||
|
||||
await patch<User[]>(
|
||||
`${Constants.projectsURI}/${id}/members`,
|
||||
members.map(m => m.id)
|
||||
);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={show} handleClose={handleClose}>
|
||||
<div className="row valign-wrapper blue">
|
||||
<div className="row valign-wrapper indigo">
|
||||
<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"
|
||||
className="right material-icons indigo lighten-3 circle"
|
||||
onClick={handleClose}
|
||||
>
|
||||
close
|
||||
|
|
@ -78,10 +78,10 @@ export const UsersModal: FC<IProps> = ({
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="modal-footer">
|
||||
<div className="modal-footer grey lighten-3">
|
||||
<input
|
||||
type="submit"
|
||||
className="modal-close waves-effect waves-green btn"
|
||||
className="modal-close waves-effect waves-green btn indigo"
|
||||
value="Done"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ interface IProps {
|
|||
}
|
||||
|
||||
export const UsersModalEntry: FC<IProps> = ({ user, setMembers, members }) => {
|
||||
// console.log(members);
|
||||
const match: (id: string) => boolean = (id: string) => {
|
||||
return Boolean(members.find(m => m.id === id));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,90 @@
|
|||
import React, { FC } from "react";
|
||||
import React, { FC, useState, useEffect } from "react";
|
||||
import { UserPage } from "../pages/UserPage";
|
||||
import { UserVM } from "../VM/UserVM";
|
||||
import { User } from "../types/User";
|
||||
import { AppFile } from "../types/AppFile";
|
||||
import { Activity } from "../types/Activity";
|
||||
import { Ticket } from "../types/Ticket";
|
||||
import { Preloader } from "../components/Preloader";
|
||||
|
||||
export const UserController: FC = () => {
|
||||
return <UserPage />;
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const user: User = {
|
||||
id: "resldsm,dgd",
|
||||
firstName: "David",
|
||||
lastName: "Whittaker",
|
||||
fullName: "David Whittaker",
|
||||
presentation: "Interface designer and front-end developer",
|
||||
creationDate: new Date().toDateString(),
|
||||
email: "dw@mail.au",
|
||||
phone: "0998765432",
|
||||
picture: require("../images/user_1.jpg"),
|
||||
projects: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Project Title",
|
||||
description: "What is it about",
|
||||
progression: 25,
|
||||
creationDate: new Date().toDateString(),
|
||||
endingDate: "2020-02-17 15:51:02.787373",
|
||||
status: "Todo",
|
||||
manager: {} as User,
|
||||
users: [] as User[],
|
||||
tickets: [] as Ticket[],
|
||||
files: [] as AppFile[],
|
||||
activities: [] as Activity[]
|
||||
}
|
||||
],
|
||||
tickets: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Client objective meeting",
|
||||
description: "Client objective meeting",
|
||||
endingDate: "2020-02-17 15:51:02.787373",
|
||||
status: "Done",
|
||||
project: {
|
||||
id: 1,
|
||||
title: "Project Title",
|
||||
description: "What is it about",
|
||||
progression: 25,
|
||||
creationDate: new Date().toDateString(),
|
||||
endingDate: "2020-02-17 15:51:02.787373",
|
||||
status: "Todo",
|
||||
manager: {} as User,
|
||||
users: [] as User[],
|
||||
tickets: [] as Ticket[],
|
||||
files: [] as AppFile[],
|
||||
activities: [] as Activity[]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Assemble Outcomes Report for client",
|
||||
description: "Assemble Outcomes Report for client",
|
||||
endingDate: "2020-02-27 15:51:02.787373",
|
||||
status: "To Do",
|
||||
project: {
|
||||
id: 1,
|
||||
title: "Project Title",
|
||||
description: "What is it about",
|
||||
progression: 25,
|
||||
creationDate: new Date().toDateString(),
|
||||
endingDate: "2020-02-17 15:51:02.787373",
|
||||
status: "Todo",
|
||||
manager: {} as User,
|
||||
users: [] as User[],
|
||||
tickets: [] as Ticket[],
|
||||
files: [] as AppFile[],
|
||||
activities: [] as Activity[]
|
||||
}
|
||||
}
|
||||
],
|
||||
activities: []
|
||||
};
|
||||
useEffect(() => {
|
||||
setTimeout(() => setIsLoading(false), 1000);
|
||||
});
|
||||
const viewModel = new UserVM(user);
|
||||
return isLoading ? <Preloader /> : <UserPage viewModel={viewModel} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const ProjectPage: FC<IProps> = ({ viewModel }) => {
|
|||
<AvatarList users={users} />
|
||||
<FloatingButton
|
||||
icon="add"
|
||||
color="grey"
|
||||
color="indigo lighten-3"
|
||||
size="small"
|
||||
onClick={() => setShowModal(true)}
|
||||
/>
|
||||
|
|
@ -59,6 +59,7 @@ export const ProjectPage: FC<IProps> = ({ viewModel }) => {
|
|||
tickets={tickets}
|
||||
files={files}
|
||||
activities={activities}
|
||||
allUsers={allUsers}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,32 @@
|
|||
import React, { FC } from "react";
|
||||
import { Header } from "../components/Header";
|
||||
import { UserVM } from "../VM/UserVM";
|
||||
import { UserHeader } from "../components/UserHeader";
|
||||
import { UserTabRouter } from "../components/UserTabRouter";
|
||||
|
||||
export const UserPage: FC = () => {
|
||||
interface IProps {
|
||||
viewModel: UserVM;
|
||||
}
|
||||
export const UserPage: FC<IProps> = ({ viewModel }) => {
|
||||
const { fullName, presentation, picture, projects, tickets } = viewModel;
|
||||
const tabNames: string[] = ["Projects", "Tickets"];
|
||||
return (
|
||||
<Header title = "Brand Concept and Design" description = "Research, ideate and present brand concepts for client consideration"/>
|
||||
// <TabView>
|
||||
<div className="section">
|
||||
<div className="container">
|
||||
<UserHeader
|
||||
picture={picture}
|
||||
fullName={fullName}
|
||||
presentation={presentation}
|
||||
/>
|
||||
<UserTabRouter
|
||||
tabNames={tabNames}
|
||||
projects={projects}
|
||||
tickets={tickets}
|
||||
/>
|
||||
</div>
|
||||
{/* // <TabView>
|
||||
// <CardList>
|
||||
// <CardList>
|
||||
// </TabView>
|
||||
// </TabView> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { Project } from "./Project";
|
||||
|
||||
export interface Ticket {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
status: string;
|
||||
plannedEnding: string;
|
||||
endingDate: string;
|
||||
project: Project;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface User {
|
|||
presentation: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
createdAt: string;
|
||||
creationDate: string;
|
||||
picture: string;
|
||||
activities: Activity[];
|
||||
projects: Project[];
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import * as creacteHistory from "history";
|
|||
import { ProjectController } from "../controllers/ProjectController";
|
||||
import { NotFoundPage } from "../pages/NotFoundPage";
|
||||
import { TestPage } from "../pages/TestPage";
|
||||
// import { UserController } from "../controllers/UserController";
|
||||
import { UserController } from "../controllers/UserController";
|
||||
// import { TicketController } from "../controllers/TicketController";
|
||||
|
||||
export const history = creacteHistory.createBrowserHistory();
|
||||
|
|
@ -20,7 +20,7 @@ export const history = creacteHistory.createBrowserHistory();
|
|||
export const AppRouter = () => {
|
||||
return (
|
||||
<Router history={history}>
|
||||
<div className="grey lighten-4">
|
||||
<div className="grey lighten-3">
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<TestPage />
|
||||
|
|
@ -28,24 +28,20 @@ export const AppRouter = () => {
|
|||
|
||||
{/* <Route path="/">
|
||||
<HomeController />
|
||||
</Route>
|
||||
</Route> */}
|
||||
<Route path="/users/:id">
|
||||
<UserController />
|
||||
</Route> */}
|
||||
</Route>
|
||||
<Route path="/projects/:id">
|
||||
<ProjectController />
|
||||
</Route>
|
||||
{/* <Route path="/tickets/:id">
|
||||
<TicketController />
|
||||
</Route> */}
|
||||
</Route> */}
|
||||
|
||||
<Route path="/401">
|
||||
<NotFoundPage />
|
||||
</Route>
|
||||
|
||||
{/* <Route path="*">
|
||||
<Redirect to="/error" />
|
||||
</Route> */}
|
||||
</Switch>
|
||||
</div>
|
||||
</Router>
|
||||
|
|
|
|||
Loading…
Reference in a new issue