Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ node_modules
/coverage
*.log
.env*
.yarn

# System Files
.DS_Store
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"nxConsole.nxWorkspacePath": ""
}
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: node-modules
58 changes: 57 additions & 1 deletion apps/backstage/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,61 @@
"projectType": "application",
"tags": [],
"// targets": "to see all targets run: nx show project backstage --web",
"targets": {}
"targets": {
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"configFile": "apps/backstage/vite.config.ts",
"outputPath": "dist/apps/backstage"
},
"configurations": {
"development": {
"mode": "development"
},
"production": {
"mode": "production"
}
}
},
"serve": {
"executor": "@nx/vite:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "backstage:build"
},
"configurations": {
"development": {
"hmr": true
},
"production": {
"buildTarget": "backstage:build:production"
}
}
},
"preview": {
"executor": "@nx/vite:preview",
"options": {
"buildTarget": "backstage:build"
}
},
"test": {
"executor": "@nx/vite:test",
"outputs": [
"{projectRoot}/coverage"
],
"options": {
"configFile": "apps/backstage/vite.config.ts",
"passWithNoTests": true,
"reportsDirectory": "coverage/apps/backstage"
}
},
"lint": {
"executor": "nx:run-commands",
"options": {
"command": "npx eslint \"apps/backstage/**/*.{ts,tsx,js,jsx}\""
}
}
}
}
5 changes: 2 additions & 3 deletions apps/backstage/src/components/MemberSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useState } from 'react';
import { Input } from '@luminova/ui';
import { Badge } from '@luminova/ui';
import { Input, Badge } from '@luminova/ui';
import { usePaginatedMembers } from '../features/members';

type MemberSelectorProps = {
Expand All @@ -17,7 +16,7 @@ const MemberSelector = ({
pageSize = 10,
}: MemberSelectorProps) => {
const [inputValue, setInputValue] = useState('');
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
usePaginatedMembers(pageSize);

const members = data?.pages.flatMap((page) => page.members) || [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ export function EditAllyDialog({ ally }: EditAllyDialogProps) {
};

// Prepare initial values for the form by excluding the 'id'
const { id, ...initialFormValues } = ally;
const initialFormValues: AllyInput = {
companyName: ally.companyName,
personInCharge: ally.personInCharge,
phone: ally.phone,
email: ally.email,
};

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
Expand All @@ -65,4 +70,4 @@ export function EditAllyDialog({ ally }: EditAllyDialogProps) {
</DialogContent>
</Dialog>
);
}
}
4 changes: 3 additions & 1 deletion apps/backstage/src/features/auth/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export const useAuth = () => {

return useQuery<AuthUser | null>({
queryKey: AUTH_QUERY_KEY,
queryFn: () => null, // Initial value, will be updated by the observer
queryFn: () => AuthService.getCurrentUser(),
staleTime: Infinity,
gcTime: Infinity,
refetchOnWindowFocus: false,
});
};
16 changes: 16 additions & 0 deletions apps/backstage/src/features/auth/services/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ const mapFirebaseUser = (user: FirebaseUser): AuthUser => ({
});

export class AuthService {
static async getCurrentUser(): Promise<AuthUser | null> {
return new Promise((resolve, reject) => {
const unsubscribe = onAuthStateChanged(
auth,
(user) => {
unsubscribe();
resolve(user ? mapFirebaseUser(user) : null);
},
(error) => {
unsubscribe();
reject(error);
},
);
});
}

static async login({ email, password }: LoginCredentials): Promise<AuthUser> {
const { user } = await signInWithEmailAndPassword(auth, email, password);
return mapFirebaseUser(user);
Expand Down
73 changes: 68 additions & 5 deletions apps/backstage/src/features/events/components/EventForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,39 @@ type Props = {
initialValues?: EventInput;
};

const toDateInputValue = (value: string | Date): string => {
if (value instanceof Date) {
return value.toISOString().split('T')[0];
}

const parsed = new Date(value);
if (!Number.isNaN(parsed.getTime())) {
return parsed.toISOString().split('T')[0];
}

return '';
};

export function EventForm({ onSubmit, isLoading, initialValues }: Props) {
const form = useForm({
const defaultDate = toDateInputValue(new Date());

const resolvedInitialValues = initialValues
? {
...initialValues,
startDate: toDateInputValue(initialValues.startDate),
endDate: toDateInputValue(initialValues.endDate),
}
: undefined;

const form = useForm<EventInput>({
resolver: zodResolver(EventInputSchema),
defaultValues: initialValues || {
defaultValues: resolvedInitialValues || {
type: 'Program',
name: '',
description: '',
scope: 'Local',
startDate: Date.now(),
endDate: Date.now(),
startDate: defaultDate,
endDate: defaultDate,
directorId: '',
coDirectorIds: [],
collaboratorIds: [],
Expand Down Expand Up @@ -120,7 +143,7 @@ export function EventForm({ onSubmit, isLoading, initialValues }: Props) {
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value ?? 'Local'}
className="flex flex-row space-x-4"
>
<FormItem className="flex items-center space-x-3 space-y-0">
Expand All @@ -143,6 +166,46 @@ export function EventForm({ onSubmit, isLoading, initialValues }: Props) {
)}
/>

<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<FormField
control={form.control}
name="startDate"
render={({ field }) => (
<FormItem>
<FormLabel>Start Date</FormLabel>
<FormControl>
<Input
type="date"
{...field}
value={field.value ?? ''}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="endDate"
render={({ field }) => (
<FormItem>
<FormLabel>End Date</FormLabel>
<FormControl>
<Input
type="date"
{...field}
value={field.value ?? ''}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>

{/* Step 3: Assign Roles */}
<FormField
control={form.control}
Expand Down
12 changes: 10 additions & 2 deletions apps/backstage/src/features/events/components/EventTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ type Props = {
isLoading: boolean;
};

const formatDate = (value: string): string => {
const date = new Date(`${value}T00:00:00`);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleDateString();
};

export function EventTable({ events, members, isLoading }: Props) {
const deleteEventMutation = useDeleteEvent();

Expand Down Expand Up @@ -81,8 +89,8 @@ export function EventTable({ events, members, isLoading }: Props) {
<TableCell>{event.type}</TableCell>
<TableCell>{event.director?.name || '-'}</TableCell>
<TableCell>{event.scope || 'N/A'}</TableCell>
<TableCell>{event.startDate}</TableCell>
<TableCell>{event.endDate}</TableCell>
<TableCell>{formatDate(event.startDate)}</TableCell>
<TableCell>{formatDate(event.endDate)}</TableCell>
<TableCell className="flex items-center gap-2">
{/* Edit Event Dialog */}
<EditEventDialog event={event} />
Expand Down
79 changes: 76 additions & 3 deletions apps/backstage/src/features/events/repositories/eventRepository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Timestamp,
addDoc,
collection,
deleteDoc,
Expand All @@ -12,15 +13,67 @@ import type { Event, EventInput } from '../types/event';

const COLLECTION_NAME = 'events';

const toDateInputValue = (date: Date): string =>
date.toISOString().split('T')[0];

const toDateFromInput = (value: string): Date =>
new Date(`${value}T00:00:00`);

const formatStoredDate = (value: unknown): string => {
if (value instanceof Timestamp) {
return toDateInputValue(value.toDate());
}

if (value && typeof (value as { toDate?: () => Date }).toDate === 'function') {
return toDateInputValue((value as { toDate: () => Date }).toDate());
}

if (typeof value === 'number') {
const parsed = new Date(value);
if (!Number.isNaN(parsed.getTime())) {
return toDateInputValue(parsed);
}
}

if (typeof value === 'string') {
const parsed = new Date(value);
if (!Number.isNaN(parsed.getTime())) {
return toDateInputValue(parsed);
}
}

return '';
};

export class EventRepository {
static async getEvents(): Promise<Event[]> {
const q = query(collection(db, COLLECTION_NAME));
const snapshot = await getDocs(q);
return snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }) as Event);
return snapshot.docs.map((docSnap) => {
const data = docSnap.data();
return {
id: docSnap.id,
type: data.type,
name: data.name,
description: data.description ?? '',
scope: data.scope ?? 'Local',
directorId: data.directorId,
coDirectorIds: data.coDirectorIds ?? [],
collaboratorIds: data.collaboratorIds ?? [],
participantIds: data.participantIds ?? [],
parentId: data.parentId ?? '',
startDate: formatStoredDate(data.startDate),
endDate: formatStoredDate(data.endDate),
};
});
}

static async addEvent(event: EventInput): Promise<string> {
const docRef = await addDoc(collection(db, COLLECTION_NAME), event);
const docRef = await addDoc(collection(db, COLLECTION_NAME), {
...event,
startDate: Timestamp.fromDate(toDateFromInput(event.startDate)),
endDate: Timestamp.fromDate(toDateFromInput(event.endDate)),
});
return docRef.id;
}

Expand All @@ -29,7 +82,27 @@ export class EventRepository {
updatedData: Partial<EventInput>,
): Promise<void> {
const docRef = doc(db, COLLECTION_NAME, id);
await updateDoc(docRef, updatedData);
const payload: Record<string, unknown> = {};

Object.entries(updatedData).forEach(([key, value]) => {
if (value !== undefined && key !== 'startDate' && key !== 'endDate') {
payload[key] = value;
}
});

if (updatedData.startDate) {
payload.startDate = Timestamp.fromDate(
toDateFromInput(updatedData.startDate),
);
}

if (updatedData.endDate) {
payload.endDate = Timestamp.fromDate(
toDateFromInput(updatedData.endDate),
);
}

await updateDoc(docRef, payload);
}

static async deleteEvent(id: string): Promise<void> {
Expand Down
Loading
Loading