Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SHEP-2] UI Created to Split a Single Campaign into Multiple Campaigns #268

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6ceee2a
Add functionality for splitting campaigns
akhan-mozilla Sep 30, 2024
edd8600
Merge branch 'main' into origin/SHEP-2-create-ui-to-split-single-camp…
akhan-mozilla Sep 30, 2024
b809429
fix migrations sequence
akhan-mozilla Sep 30, 2024
d004cb7
Merge branch 'main' into origin/SHEP-2-create-ui-to-split-single-camp…
akhan-mozilla Sep 30, 2024
06c2935
fix migrations sequence
akhan-mozilla Sep 30, 2024
c05541b
prevent unexpected error when empty data for products
akhan-mozilla Oct 1, 2024
626baa3
removed unused return data
akhan-mozilla Oct 1, 2024
77e548d
Merge branch 'main' into origin/SHEP-2-create-ui-to-split-single-camp…
akhan-mozilla Oct 1, 2024
872451d
fix campaign filter error
akhan-mozilla Oct 1, 2024
e303fe3
Merge branch 'main' into origin/SHEP-2-create-ui-to-split-single-camp…
akhan-mozilla Oct 1, 2024
acded75
fix campaigns error
akhan-mozilla Oct 1, 2024
dbe819c
env file configs
akhan-mozilla Oct 1, 2024
c54199d
deal validation error message update
akhan-mozilla Oct 2, 2024
4fd8929
Updated validation and changed date field order
akhan-mozilla Oct 3, 2024
afae0da
made ads ops person notes and flight id feilds optional
akhan-mozilla Oct 3, 2024
c08a8e3
Add validation for impressions sold, similar to net spend on the spli…
akhan-mozilla Oct 3, 2024
17e76f4
text fixes
akhan-mozilla Oct 3, 2024
abb0ae5
Merge branch 'main' into origin/SHEP-2-create-ui-to-split-single-camp…
akhan-mozilla Oct 4, 2024
b2fac88
Merge branch 'main' into origin/SHEP-2-create-ui-to-split-single-camp…
akhan-mozilla Oct 4, 2024
35121c0
Add null checks in form validation for notes and ad_ops_person
akhan-mozilla Oct 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ad-ops-dashboard/src/components/Dialogs/FormDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface FormDialogProps {
children: ReactNode;
open: boolean;
handleClose: () => void;
maxWidth?: "sm" | "md" | "lg" | "xl";
}

const Container = styled(Box)`
Expand All @@ -33,6 +34,7 @@ export default function FormDialog({
children,
open,
handleClose,
maxWidth,
}: FormDialogProps) {
return (
<Dialog
Expand All @@ -43,6 +45,7 @@ export default function FormDialog({
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
aria-modal
maxWidth={maxWidth}
>
<DialogTitle>
<Container>
Expand Down
23 changes: 15 additions & 8 deletions ad-ops-dashboard/src/components/Forms/CampaignForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export default function CampaignForm({
useForm<CampaignFormSchema>({
resolver: zodResolver(campaignFormSchema),
defaultValues: {
notes: formData.notes,
ad_ops_person: formData.ad_ops_person,
notes: formData.notes ?? "",
ad_ops_person: formData.ad_ops_person ?? "",
kevel_flight_id: formData.kevel_flight_id,
impressions_sold: formData.impressions_sold,
net_spend: formData.net_spend,
Expand Down Expand Up @@ -63,7 +63,7 @@ export default function CampaignForm({
selectedDeal = formData.deal === watchDeal ? formData.deal : watchDeal;
}

if (selectedDeal) {
if (Array.isArray(campaigns) && selectedDeal) {
const filtered = campaigns
.filter((item) => item.deal === selectedDeal)
.filter((item) => !isUpdate || item.id !== formData.id);
Expand Down Expand Up @@ -111,10 +111,12 @@ export default function CampaignForm({
}
};

const deals = boostrDeals?.map((deal: BoostrDeal) => ({
label: deal.name,
value: deal.id,
}));
const deals = Array.isArray(boostrDeals)
? boostrDeals.map((deal: BoostrDeal) => ({
label: deal.name,
value: deal.id,
}))
: [];

return (
<Box>
Expand All @@ -124,6 +126,7 @@ export default function CampaignForm({
name="ad_ops_person"
label="Ad Ops Person"
control={control}
fullWidth
/>
</Box>
<Box mt={2}>
Expand All @@ -132,6 +135,7 @@ export default function CampaignForm({
label="Kevel Flight Id"
control={control}
type="number"
fullWidth
/>
</Box>
<Box mt={2}>
Expand All @@ -140,6 +144,7 @@ export default function CampaignForm({
label="Impressions Sold"
control={control}
type="number"
fullWidth
/>
</Box>
<Box mt={2}>
Expand All @@ -148,10 +153,11 @@ export default function CampaignForm({
label="Net Spend"
control={control}
type="number"
fullWidth
/>
</Box>
<Box mt={2}>
<TextInput name="seller" label="Seller" control={control} />
<TextInput name="seller" label="Seller" control={control} fullWidth />
</Box>
<Box mt={2}>
<DateInput
Expand All @@ -176,6 +182,7 @@ export default function CampaignForm({
control={control}
rows={3}
multiline
fullWidth
/>
</Box>
<Box mt={2}>
Expand Down
201 changes: 201 additions & 0 deletions ad-ops-dashboard/src/components/Forms/SplitCampaignForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import React from "react";
import { Box, Button, Grid2 } from "@mui/material";
import { Add, Remove } from "@mui/icons-material";
import { useFieldArray, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { styled } from "@mui/system";
import {
CampaignFormSchema,
SplitFormSchema,
splitFormSchema,
} from "../../utils/schemas/campaignFormSchema";
import TextInput from "../Inputs/TextInput";
import DateInput from "../Inputs/DateInput";
import { useSplitCampaignMutation } from "../../data/campaigns";
import Tooltip from "@mui/material/Tooltip";

interface SplitCampaignProps {
handleClose: () => void;
formData: CampaignFormSchema;
}

const StyledBox = styled(Box)(() => ({
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}));

const StyledButton = styled(Button)(() => ({
marginRight: "3.8rem",
}));

export default function SplitCampaignForm({
formData,
handleClose,
}: SplitCampaignProps) {
const splitMutation = useSplitCampaignMutation();

const { control, handleSubmit, reset } = useForm<SplitFormSchema>({
resolver: zodResolver(splitFormSchema),
defaultValues: {
campaigns: [
{
id: formData.id,
impressions_sold: formData.impressions_sold || "",
net_spend: formData.net_spend || "",
kevel_flight_id: formData.kevel_flight_id || "",
ad_ops_person: formData.ad_ops_person || "",
seller: formData.seller || "",
start_date: formData.start_date || "",
end_date: formData.end_date || "",
notes: formData.notes || "",
deal: formData.deal || undefined,
},
],
},
});

const onSubmitHandler = (data: SplitFormSchema) => {
const updatedData = {
...data,
deal: formData.deal,
};

splitMutation.mutate(updatedData, {
onSuccess: () => {
reset();
handleClose();
},
});
};

const { fields, append, remove } = useFieldArray({
control,
name: "campaigns",
});

return (
<Box mt="2rem">
<Box component="form" onSubmit={handleSubmit(onSubmitHandler)}>
<Grid2 container spacing={2}>
{fields.map((field, index) => (
<React.Fragment key={field.id}>
<Grid2 size={2}>
<TextInput
name={`campaigns.${index}.ad_ops_person`}
label="Ad Ops Person"
control={control}
fullWidth
/>
</Grid2>
<Grid2 size={1}>
<TextInput
name={`campaigns.${index}.kevel_flight_id`}
label="Kevel Flight Id"
type="number"
control={control}
fullWidth
/>
</Grid2>
<Grid2 size={1}>
<TextInput
name={`campaigns.${index}.impressions_sold`}
label="Impressions Sold"
type="number"
control={control}
fullWidth
/>
</Grid2>
<Grid2 size={1}>
<TextInput
name={`campaigns.${index}.net_spend`}
label="Net spend"
type="number"
control={control}
fullWidth
/>
</Grid2>
<Grid2 size={2}>
<TextInput
name={`campaigns.${index}.seller`}
label="Seller"
control={control}
fullWidth
/>
</Grid2>
<Grid2 size={3}>
<StyledBox>
<Box>
<DateInput
control={control}
label="Start Date"
name={`campaigns.${index}.start_date`}
/>
</Box>
<Box sx={{ marginLeft: "1rem" }}>
<DateInput
control={control}
label="End Date"
name={`campaigns.${index}.end_date`}
/>
</Box>
</StyledBox>
</Grid2>
<Grid2 size={2}>
<StyledBox>
<Box>
<TextInput
name={`campaigns.${index}.notes`}
label="Notes"
control={control}
/>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
{index == 0 ? (
<Tooltip title="Split Campaign" placement="top" arrow>
<Button
onClick={() =>
append({
impressions_sold: "",
net_spend: "",
kevel_flight_id: "",
ad_ops_person: "",
seller: "",
start_date: "",
end_date: "",
notes: "",
deal: formData.deal,
})
}
>
<Add fontSize="large" />
</Button>
</Tooltip>
) : (
<Tooltip title="Remove Campaign" placement="top" arrow>
<Button onClick={() => remove(index)}>
<Remove fontSize="large" color="error" />
</Button>
</Tooltip>
)}
</Box>
</StyledBox>
</Grid2>
</React.Fragment>
))}
</Grid2>
<Box mt="2rem" display="flex" justifyContent="flex-end" width="100%">
<StyledButton variant="contained" type="submit">
Save
</StyledButton>
</Box>
</Box>
</Box>
);
}
15 changes: 3 additions & 12 deletions ad-ops-dashboard/src/components/Inputs/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,13 @@ export default function TextInput({
<Controller
name={name}
control={control}
render={({
field: { onChange, onBlur, value, ref },
fieldState: { error },
}) => (
render={({ field, fieldState: { error } }) => (
<TextField
onChange={(e) =>
{ onChange(props.type === "number" ? +e.target.value : e.target.value); }
}
onBlur={onBlur}
value={value}
inputRef={ref}
fullWidth
label={label}
{...field}
{...props}
error={!!error}
helperText={error?.message}
label={label}
/>
)}
/>
Expand Down
4 changes: 2 additions & 2 deletions ad-ops-dashboard/src/config/routes.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ const BASE_URL = import.meta.env.VITE_API_BASE_URL;

export const apiRoutes = {
campaigns: `${BASE_URL}/campaigns/`,
campaign: (id: number | undefined) =>
`${BASE_URL}/campaigns/${id}/`,
campaign: (id: number | undefined) => `${BASE_URL}/campaigns/${id}/`,
splitCampaigns: `${BASE_URL}/campaigns/split/`,
boostrDeals: `${BASE_URL}/deals/`,
products: `${BASE_URL}/products/`,
};
30 changes: 29 additions & 1 deletion ad-ops-dashboard/src/data/campaigns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "react-toastify";
import { apiRoutes } from "../config/routes.config";
import { CampaignFormSchema } from "../utils/schemas/campaignFormSchema";
import {
CampaignFormSchema,
SplitFormSchema,
} from "../utils/schemas/campaignFormSchema";

export const useGetCampaignsQuery = () => {
const getCampaigns = useQuery({
queryKey: ["campaigns"],
Expand All @@ -14,6 +18,30 @@ export const useGetCampaignsQuery = () => {

return getCampaigns;
};

export const useSplitCampaignMutation = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (data: SplitFormSchema) => {
return axios.post(apiRoutes.splitCampaigns, data);
},
onError: (error: any) => {
if (error.response?.data) {
const errorMessage =
error.response.data.non_field_errors?.[0] || "An error occurred";
toast.error(errorMessage);
} else {
toast.error("An unexpected error occurred");
}
},
onSuccess: () => {
toast.success("Campaign splitted successfully.");
queryClient.invalidateQueries({ queryKey: ["campaigns"] });
},
});
};

export const useCreateCampaignMutation = () => {
const queryClient = useQueryClient();

Expand Down
Loading