first commit
All checks were successful
CI / backend (push) Successful in 13m19s
CI / frontend (push) Successful in 11m3s
CI / docker (push) Successful in 1m58s

This commit is contained in:
Cauê Faleiros
2026-06-03 16:31:42 -03:00
commit 8c7e5fbbe4
92 changed files with 18226 additions and 0 deletions

65
frontend/src/lib/api.ts Normal file
View File

@@ -0,0 +1,65 @@
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080/api/v1";
type RequestOptions = RequestInit & {
token?: string | null;
};
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
const headers = new Headers(options.headers);
headers.set("Content-Type", "application/json");
if (options.token) {
headers.set("Authorization", `Bearer ${options.token}`);
}
const response = await fetch(`${API_BASE_URL}${path}`, {
...options,
headers,
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const message =
typeof data === "object" && data !== null && "message" in data
? String(data.message)
: "Nao foi possivel concluir a solicitacao.";
throw new ApiError(message, response.status);
}
return data as T;
}
export async function apiFormRequest<T>(path: string, formData: FormData, token?: string | null): Promise<T> {
const headers = new Headers();
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
const response = await fetch(`${API_BASE_URL}${path}`, {
method: "POST",
headers,
body: formData,
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
const message =
typeof data === "object" && data !== null && "message" in data
? String(data.message)
: "Nao foi possivel concluir a solicitacao.";
throw new ApiError(message, response.status);
}
return data as T;
}
export class ApiError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.name = "ApiError";
this.status = status;
}
}

62
frontend/src/lib/auth.ts Normal file
View File

@@ -0,0 +1,62 @@
import { apiRequest } from "@/lib/api";
const TOKEN_STORAGE_KEY = "mira_access_token";
export type AuthUser = {
id: string;
email: string;
name: string;
role: "super_admin" | "agency_user" | "client_viewer";
status: string;
};
type LoginResponse = {
access_token: string;
token_type: "Bearer";
user: AuthUser;
};
type MeResponse = {
user: AuthUser;
};
export function getStoredToken() {
return localStorage.getItem(TOKEN_STORAGE_KEY);
}
export function storeToken(token: string) {
localStorage.setItem(TOKEN_STORAGE_KEY, token);
}
export function clearStoredToken() {
localStorage.removeItem(TOKEN_STORAGE_KEY);
}
export async function login(email: string, password: string) {
const response = await apiRequest<LoginResponse>("/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
storeToken(response.access_token);
return response;
}
export async function acceptInvitation(token: string, name: string, password: string) {
const response = await apiRequest<LoginResponse>("/auth/invitations/accept", {
method: "POST",
body: JSON.stringify({ token, name, password }),
});
storeToken(response.access_token);
return response;
}
export async function loadCurrentUser(token: string) {
const response = await apiRequest<MeResponse>("/me", {
method: "GET",
token,
});
return response.user;
}

View File

@@ -0,0 +1,256 @@
import { API_BASE_URL, apiFormRequest, apiRequest } from "@/lib/api";
export type Holiday = {
id: string;
name: string;
date: string;
type: string;
source: string;
};
export type CustomDate = {
id: string;
client_id: string | null;
name: string;
description: string;
date: string;
recurs_annually: boolean;
visibility: "agency" | "client";
category: string;
created_at: string;
updated_at: string;
};
export type CalendarItem = {
id: string;
client_id: string;
client_name: string;
title: string;
description: string;
content_type: string;
status: "rascunho" | "planejado" | "em_producao" | "em_revisao" | "aprovado" | "publicado" | "cancelado";
owner_id: string | null;
scheduled_date: string;
scheduled_at: string | null;
copy_text: string;
internal_notes: string;
client_notes: string;
created_at: string;
updated_at: string;
};
export type Attachment = {
id: string;
calendar_item_id: string;
original_filename: string;
stored_filename: string;
mime_type: string;
size_bytes: number;
storage_driver: string;
created_by: string | null;
created_at: string;
};
export type CreateCustomDateInput = {
client_id?: string;
name: string;
description?: string;
date: string;
recurs_annually: boolean;
visibility: "agency" | "client";
category: string;
};
export type UpdateCustomDateInput = {
client_id?: string;
name: string;
description?: string;
date: string;
recurs_annually: boolean;
visibility: "agency" | "client";
};
export type CreateCalendarItemInput = {
client_id: string;
title: string;
description?: string;
content_type: string;
status: CalendarItem["status"];
scheduled_date: string;
copy_text?: string;
internal_notes?: string;
client_notes?: string;
};
export type UpdateCalendarItemInput = Partial<{
title: string;
description: string;
status: CalendarItem["status"];
scheduled_date: string;
copy_text: string;
internal_notes: string;
client_notes: string;
}>;
type HolidaysResponse = {
holidays: Holiday[];
};
type CustomDatesResponse = {
custom_dates: CustomDate[];
};
type CustomDateResponse = {
custom_date: CustomDate;
};
type CalendarItemsResponse = {
items: CalendarItem[];
};
type CalendarItemResponse = {
item: CalendarItem;
};
type AttachmentsResponse = {
attachments: Attachment[];
};
type AttachmentResponse = {
attachment: Attachment;
};
export async function listHolidays(token: string, year: number) {
const response = await apiRequest<HolidaysResponse>(`/calendar/holidays?year=${year}`, {
method: "GET",
token,
});
return response.holidays;
}
export async function listCustomDates(token: string, year: number, clientId?: string | null) {
const params = new URLSearchParams({ year: String(year) });
if (clientId) {
params.set("client_id", clientId);
}
const response = await apiRequest<CustomDatesResponse>(`/calendar/custom-dates?${params.toString()}`, {
method: "GET",
token,
});
return response.custom_dates;
}
export async function createCustomDate(token: string, input: CreateCustomDateInput) {
const response = await apiRequest<CustomDateResponse>("/calendar/custom-dates", {
method: "POST",
token,
body: JSON.stringify(input),
});
return response.custom_date;
}
export async function updateCustomDate(token: string, customDateId: string, input: UpdateCustomDateInput) {
const response = await apiRequest<CustomDateResponse>(`/calendar/custom-dates/${customDateId}`, {
method: "PATCH",
token,
body: JSON.stringify(input),
});
return response.custom_date;
}
export async function deleteCustomDate(token: string, customDateId: string) {
await apiRequest<{ message: string }>(`/calendar/custom-dates/${customDateId}`, {
method: "DELETE",
token,
});
}
export async function listCalendarItems(token: string, year: number, clientId?: string | null) {
const params = new URLSearchParams({ year: String(year) });
if (clientId) {
params.set("client_id", clientId);
}
const response = await apiRequest<CalendarItemsResponse>(`/calendar/items?${params.toString()}`, {
method: "GET",
token,
});
return response.items;
}
export async function listCalendarItemsByRange(token: string, from: string, to: string, clientId?: string | null) {
const params = new URLSearchParams({ from, to });
if (clientId) {
params.set("client_id", clientId);
}
const response = await apiRequest<CalendarItemsResponse>(`/calendar/items?${params.toString()}`, {
method: "GET",
token,
});
return response.items;
}
export async function createCalendarItem(token: string, input: CreateCalendarItemInput) {
const response = await apiRequest<CalendarItemResponse>("/calendar/items", {
method: "POST",
token,
body: JSON.stringify(input),
});
return response.item;
}
export async function getCalendarItem(token: string, itemId: string) {
const response = await apiRequest<CalendarItemResponse>(`/calendar/items/${itemId}`, {
method: "GET",
token,
});
return response.item;
}
export async function updateCalendarItem(token: string, itemId: string, input: UpdateCalendarItemInput) {
const response = await apiRequest<CalendarItemResponse>(`/calendar/items/${itemId}`, {
method: "PATCH",
token,
body: JSON.stringify(input),
});
return response.item;
}
export async function cancelCalendarItem(token: string, itemId: string) {
await apiRequest<{ message: string }>(`/calendar/items/${itemId}`, {
method: "DELETE",
token,
});
}
export async function listAttachments(token: string, itemId: string) {
const response = await apiRequest<AttachmentsResponse>(`/calendar/items/${itemId}/attachments`, {
method: "GET",
token,
});
return response.attachments;
}
export async function uploadAttachment(token: string, itemId: string, file: File) {
const formData = new FormData();
formData.set("file", file);
const response = await apiFormRequest<AttachmentResponse>(`/calendar/items/${itemId}/attachments`, formData, token);
return response.attachment;
}
export function attachmentDownloadURL(attachmentId: string) {
return `${API_BASE_URL}/calendar/attachments/${attachmentId}/download`;
}

View File

@@ -0,0 +1,79 @@
import { apiRequest } from "@/lib/api";
export type Client = {
id: string;
name: string;
slug: string;
status: "active" | "archived";
website_url: string;
instagram_url: string;
facebook_url: string;
linkedin_url: string;
color: string;
notes: string;
created_at: string;
updated_at: string;
};
type ClientsResponse = {
clients: Client[];
};
type ClientResponse = {
client: Client;
};
export async function listClients(token: string) {
const response = await apiRequest<ClientsResponse>("/clients", {
method: "GET",
token,
});
return response.clients;
}
export async function createClient(token: string, name: string) {
const response = await apiRequest<ClientResponse>("/clients", {
method: "POST",
token,
body: JSON.stringify({ name }),
});
return response.client;
}
export async function updateClient(token: string, client: Client) {
const response = await apiRequest<ClientResponse>(`/clients/${client.id}`, {
method: "PATCH",
token,
body: JSON.stringify({
name: client.name,
status: client.status,
website_url: client.website_url,
instagram_url: client.instagram_url,
facebook_url: client.facebook_url,
linkedin_url: client.linkedin_url,
color: client.color,
notes: client.notes,
}),
});
return response.client;
}
export async function archiveClient(token: string, client: Client) {
const response = await apiRequest<ClientResponse>(`/clients/${client.id}`, {
method: "PATCH",
token,
body: JSON.stringify({ ...client, status: "archived" }),
});
return response.client;
}
export async function deleteClient(token: string, clientId: string) {
await apiRequest<{ message: string }>(`/clients/${clientId}`, {
method: "DELETE",
token,
});
}

54
frontend/src/lib/users.ts Normal file
View File

@@ -0,0 +1,54 @@
import { apiRequest } from "@/lib/api";
import type { AuthUser } from "@/lib/auth";
export type Invitation = {
id: string;
email: string;
role: "agency_user" | "client_viewer";
client_id: string | null;
client_name: string | null;
expires_at: string;
accepted_at: string | null;
created_at: string;
invitation_url?: string;
};
type UsersResponse = {
users: AuthUser[];
};
type InvitationsResponse = {
invitations: Invitation[];
};
type InvitationResponse = {
invitation: Invitation;
};
export async function listUsers(token: string) {
const response = await apiRequest<UsersResponse>("/users", {
method: "GET",
token,
});
return response.users;
}
export async function listInvitations(token: string) {
const response = await apiRequest<InvitationsResponse>("/users/invitations", {
method: "GET",
token,
});
return response.invitations;
}
export async function createInvitation(token: string, input: { email: string; role: Invitation["role"]; client_id?: string }) {
const response = await apiRequest<InvitationResponse>("/users/invitations", {
method: "POST",
token,
body: JSON.stringify(input),
});
return response.invitation;
}

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}