first commit
This commit is contained in:
65
frontend/src/lib/api.ts
Normal file
65
frontend/src/lib/api.ts
Normal 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
62
frontend/src/lib/auth.ts
Normal 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;
|
||||
}
|
||||
256
frontend/src/lib/calendar.ts
Normal file
256
frontend/src/lib/calendar.ts
Normal 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`;
|
||||
}
|
||||
79
frontend/src/lib/clients.ts
Normal file
79
frontend/src/lib/clients.ts
Normal 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
54
frontend/src/lib/users.ts
Normal 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;
|
||||
}
|
||||
6
frontend/src/lib/utils.ts
Normal file
6
frontend/src/lib/utils.ts
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user