The problem here is essentially that the scales of the plot in the x axis is continuous and so the usual width of 1 for position dodge is unsuitable as the 3 bars take up the space of nearly 30 days. I have tried replacing the width argument with 27. This solution had some success.
library(tidyverse)
data <- structure(list(Date = structure(c(18717, 18717, 18717, 18747,
18747, 18747, 18778, 18778, 18778), class = "Date"), Category = c("Total PSCE",
"Businesses", "Households", "Total PSCE", "Businesses", "Households",
"Total PSCE", "Businesses", "Households"), Forecast = c(2.3,
3.5, 2.5, 3.4, 5.4, 3.7, 3.4, 5.1, 3.8)), row.names = c(NA, -9L
), class = c("tbl_df", "tbl", "data.frame"))
ggplot(data, aes(Date, Forecast, fill = Category)) +
geom_col(position=position_dodge(27)) +
geom_text(aes(label = Forecast), colour = "white", size = 3, vjust = 2, position = position_dodge(27))

Created on 2021-04-07 by the reprex package (v2.0.0)
An alternate solution would be to make the month variable a categorical variable. I like to use the function month() from the lubridate package for this. I have demonstrated how this makes the widths 1 as expected below. (The as.factor() function could be used to acheive a similar effect).
library(tidyverse)
library(lubridate)
data <- structure(list(Date = structure(c(18717, 18717, 18717, 18747,
18747, 18747, 18778, 18778, 18778), class = "Date"), Category = c("Total PSCE",
"Businesses", "Households", "Total PSCE", "Businesses", "Households",
"Total PSCE", "Businesses", "Households"), Forecast = c(2.3,
3.5, 2.5, 3.4, 5.4, 3.7, 3.4, 5.1, 3.8)), row.names = c(NA, -9L
), class = c("tbl_df", "tbl", "data.frame"))
ggplot(data, aes(month(Date, label = T, abbr = T), Forecast, fill = Category)) +
geom_col(position=position_dodge()) +
geom_text(aes(label = Forecast), colour = "white", size = 3, vjust = 2, position = position_dodge(0.9))

Created on 2021-04-07 by the reprex package (v2.0.0)