diff --git a/frontend/src/models/cost/additional-cost.ts b/frontend/src/models/cost/additional-cost.ts new file mode 100644 index 0000000..ea8debd --- /dev/null +++ b/frontend/src/models/cost/additional-cost.ts @@ -0,0 +1,5 @@ +export interface AdditionalCost { + id: number + description: string + amount: number +} \ No newline at end of file diff --git a/frontend/src/models/index.ts b/frontend/src/models/index.ts index a85ff5d..952ffa4 100644 --- a/frontend/src/models/index.ts +++ b/frontend/src/models/index.ts @@ -35,3 +35,6 @@ export interface SportCenterModel { name: string location: string } + + +export type { AdditionalCost } from './cost/additional-cost' \ No newline at end of file diff --git a/frontend/src/screens/dashboard/additional-cost-editor.tsx b/frontend/src/screens/dashboard/additional-cost-editor.tsx new file mode 100644 index 0000000..aad23d1 --- /dev/null +++ b/frontend/src/screens/dashboard/additional-cost-editor.tsx @@ -0,0 +1,107 @@ +import { + ActionIcon, + Button, + NumberInput, + Table, + TextInput, +} from "@mantine/core"; +import React, { useRef, useState } from "react"; +import { FiPlusCircle, FiTrash2 } from "react-icons/fi"; +import { IoSave } from "react-icons/io5"; +import { AdditionalCost } from "../../models"; + +interface Props { + onSaveClick?: (costs: AdditionalCost[]) => void; +} + +const AdditionalCostEditor: React.FC = ({ onSaveClick }) => { + const [description, setDescription] = useState(""); + const [amount, setAmount] = useState(0); + const [costs, setCosts] = useState([]); + const nameRef = useRef(null!); + + const handleRemove = (id: number) => { + setCosts(costs.filter((c) => c.id !== id)); + }; + + const handleSubmit = (e: any) => { + e.preventDefault(); + + setCosts([ + ...costs, + { + id: costs.length + 1, + description, + amount, + }, + ]); + + setDescription(""); + setAmount(0); + nameRef.current.focus(); + }; + + const handleSave = () => { + if (onSaveClick) { + onSaveClick(costs); + } + }; + + return ( +
+
+ setDescription(e.currentTarget.value)} + /> + setAmount(+e)} + /> + + + +
+
+ + + + Name + Amount + + + + + {costs.map((item) => ( + + {item.description} + £{item.amount} + + handleRemove(item.id)} + > + + + + + ))} + +
+
+ +
+ ); +}; + +export default AdditionalCostEditor; diff --git a/frontend/src/screens/dashboard/match-figure.tsx b/frontend/src/screens/dashboard/match-figure.tsx index aefd4df..33e6005 100644 --- a/frontend/src/screens/dashboard/match-figure.tsx +++ b/frontend/src/screens/dashboard/match-figure.tsx @@ -6,20 +6,27 @@ interface Prop { label: string; figure?: string; icon?: React.ReactNode; - onEdit?: () => void; + onActionClick?: () => void; + actionIcon?: React.ReactNode; } -const MatchFigure: React.FC = ({ figure, label, icon, onEdit }) => { +const MatchFigure: React.FC = ({ + figure, + label, + icon, + onActionClick, + actionIcon, +}) => { return ( -
+
{icon} {label}
- {onEdit && ( + {onActionClick && (
- onEdit()}> - + onActionClick()}> + {actionIcon ? actionIcon : }
)} diff --git a/frontend/src/screens/dashboard/match-list-content.tsx b/frontend/src/screens/dashboard/match-list-content.tsx index 23d4a73..152e14d 100644 --- a/frontend/src/screens/dashboard/match-list-content.tsx +++ b/frontend/src/screens/dashboard/match-list-content.tsx @@ -1,13 +1,25 @@ import httpService from "../../common/http-service"; import MatchFigure from "./match-figure"; -import React, { useMemo } from "react"; +import React, { useMemo, useState } from "react"; import ToggleButton from "../../components/toggle-button"; -import { Alert, Button, Modal, NumberInput } from "@mantine/core"; import { FaCashRegister } from "react-icons/fa"; -import { FiDollarSign } from "react-icons/fi"; -import { MatchSummaryModel, RegistrationModel } from "../../models"; +import { FiDollarSign, FiTrash2 } from "react-icons/fi"; import { useDisclosure } from "@mantine/hooks"; import { useMutation } from "@tanstack/react-query"; +import { + ActionIcon, + Alert, + Button, + Modal, + NumberInput, + Table, + TextInput, +} from "@mantine/core"; +import { + AdditionalCost, + MatchSummaryModel, + RegistrationModel, +} from "../../models"; import { IoBan, IoBaseball, @@ -20,7 +32,7 @@ import { useMatchCostQuery, useRegistrationsByMatchQuery, } from "../../hooks/queries"; -import { useState } from "react"; +import AdditionalCostEditor from "./additional-cost-editor"; interface Prop { match: MatchSummaryModel; @@ -28,7 +40,6 @@ interface Prop { const MatchListContent: React.FC = ({ match }) => { const [currentCost, setCurrentCost] = useState(0); - const [costOpened, { open: openCost, close: closeCost }] = useDisclosure(false); @@ -37,11 +48,13 @@ const MatchListContent: React.FC = ({ match }) => { { open: openAdditionalCost, close: closeAdditionalCost }, ] = useDisclosure(false); - const { data: registrations, refetch: refetchRegistrations } = - useRegistrationsByMatchQuery(match.matchId); + const { data: registrations, refetch: reload } = useRegistrationsByMatchQuery( + match.matchId, + ); - const { data: cost, refetch: refetchCost } = useMatchCostQuery(match.matchId); - const { data: additionalCost } = useMatchAdditionalCostQuery(match.matchId); + const { data: cost, refetch: reloadCost } = useMatchCostQuery(match.matchId); + const { data: additionalCost, refetch: reloadAdditionalCost } = + useMatchAdditionalCostQuery(match.matchId); const statPercentage = useMemo(() => { return Math.round( @@ -70,27 +83,36 @@ const MatchListContent: React.FC = ({ match }) => { return totalPlayer === 0 ? 0 : total / totalPlayer; }, [cost, additionalCost, registrations]); + const handleSaveAdditionalCosts = async (costs: AdditionalCost[]) => { + await httpService.put( + `api/v1/matches/${match.matchId}/additional-costs`, + costs, + ); + closeAdditionalCost(); + reloadAdditionalCost(); + }; + const registerMut = useMutation({ - onSuccess: refetchRegistrations, + onSuccess: reload, mutationFn: (model: RegistrationModel) => httpService.post("api/v1/registrations", model), }); const unregisterMut = useMutation({ - onSuccess: refetchRegistrations, + onSuccess: reload, mutationFn: (registrationId: number) => httpService.del(`api/v1/registrations/${registrationId}`), }); const paidMut = useMutation({ - onSuccess: refetchRegistrations, + onSuccess: reload, mutationFn: (registrationId: number) => { return httpService.put(`api/v1/registrations/${registrationId}/paid`, {}); }, }); const unpaidMut = useMutation({ - onSuccess: refetchRegistrations, + onSuccess: reload, mutationFn: (registrationId: number) => { return httpService.put( `api/v1/registrations/${registrationId}/unpaid`, @@ -101,7 +123,7 @@ const MatchListContent: React.FC = ({ match }) => { const updateCostMut = useMutation({ onSuccess() { - refetchCost(); + reloadCost(); closeCost(); }, mutationFn: (model: { matchId: number; cost: number }) => { @@ -113,8 +135,8 @@ const MatchListContent: React.FC = ({ match }) => { return (
-
-
+
+
= ({ match }) => {
-
+
} label="Total players" @@ -157,19 +179,19 @@ const MatchListContent: React.FC = ({ match }) => { icon={} label="Cost" figure={`£${cost ?? 0}`} - onEdit={openCost} + onActionClick={openCost} > } - label="Addtional Cost" + label="Addtional cost" figure={`£${additionalCost ?? 0}`} - onEdit={openAdditionalCost} + onActionClick={openAdditionalCost} >
-
+
{registrations && registrations.map((reg) => { return ( @@ -242,9 +264,9 @@ const MatchListContent: React.FC = ({ match }) => { - +
); diff --git a/handler/match.go b/handler/match.go index 624f758..b180399 100644 --- a/handler/match.go +++ b/handler/match.go @@ -128,21 +128,25 @@ func (h *MatchHandler) GetAdditionalCost(c *gin.Context) { func (h *MatchHandler) CreateAdditionalCost(c *gin.Context) { matchId := params.Get(c, "matchId") - dto := dto.AdditionalCostDto{} - if err := c.BindJSON(&dto); err != nil { - c.AbortWithStatus(http.StatusBadRequest) + costs := []dto.AdditionalCostDto{} + if err := c.BindJSON(&costs); err != nil { + h.logger.Debugf("error parse dto: %v", err.Error()) + c.AbortWithError(http.StatusBadRequest, err) return } match := domain.Match{} - if err := h.db.Find(&match, matchId).Error; err != nil { + if err := h.db.Preload("AdditionalCosts").Find(&match, matchId).Error; err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ "error": "match not found", }) return } - match.AddCost(dto.Description, dto.Amount) + for _, c := range costs { + match.AddCost(c.Description, c.Amount) + } + h.db.Save(&match) c.JSON(http.StatusOK, match) }