Skip to content

Commit

Permalink
[FS-260]: Visit History (#139)
Browse files Browse the repository at this point in the history
This PR visits history.

---------

Co-authored-by: ASafaierad <frontendmonster@gmail.com>
  • Loading branch information
AmirabbasJ and ASafaeirad authored Feb 16, 2024
1 parent 369aae7 commit 2c1c539
Show file tree
Hide file tree
Showing 81 changed files with 4,671 additions and 2,813 deletions.
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ module.exports = init({
modules: {
auto: true,
esm: true,
disableExpensiveRules: !process.env.CI && !process.env.HUSKY,
typescript: {
parserProject: ['./tsconfig.eslint.json'],
resolverProject: ['./tsconfig.json'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const householdDetailIds = {
memberFormTab: 'member-form-tab',
householderTab: 'householder-tab',
visitsTab: 'visits-tab',
nameField: 'household-name-input',
severityField: 'household-severity-input',
statusField: 'household-status-field',
Expand Down
6 changes: 6 additions & 0 deletions app/Dashboard/Households/HouseholdDetail/HouseholdDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { InformationBadge } from '../../_components/InformationBadge';
import { SeverityBadge } from '../../_components/SeverityBadge';
import { openDeleteHouseholdModal } from '../_components/DeleteHouseholdModal';
import { HouseholderDetail } from './_components/HouseholderDetail';
import { HouseholderVisits } from './_components/HouseholderVisits';
import { MemberList } from './_components/MemberList';
import { householdDetailIds as ids } from './HouseholdDetail.ids';
import { householdNotifications } from './householdNotifications';
Expand Down Expand Up @@ -296,6 +297,11 @@ export const HouseholdDetail = () => {
panel: <MemberList householdId={household.id} />,
id: ids.memberFormTab,
},
{
tab: <Title order={5}>{t.tabs.visitsTitle}</Title>,
panel: <HouseholderVisits householdId={household.id} />,
id: ids.visitsTab,
},
]}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AddHouseholderVisitButton } from './AddHouseholderVisitButton';
import { addHouseholderVisitButtonId } from './AddHouseholderVisitButton.ids';

describe('Create Project Button', () => {
beforeEach(() => {
cy.mount(<AddHouseholderVisitButton householdId="null" />);
});

it('should be visible to users', () => {
cy.findByTestId(addHouseholderVisitButtonId).should('exist');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const addHouseholderVisitButtonId = 'add-householder-visit-button';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Meta, StoryObj } from '@storybook/react';

import { AddHouseholderVisitButton } from './AddHouseholderVisitButton';

export default {
component: AddHouseholderVisitButton,
} as Meta<typeof AddHouseholderVisitButton>;

type Story = StoryObj<typeof AddHouseholderVisitButton>;

export const Default: Story = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PlusIcon } from '@camp/icons';
import { messages } from '@camp/messages';
import { tid } from '@camp/test';
import { Button } from '@mantine/core';

import { openAddHouseholderVisitModal } from '../AddHouseholderVisitModal';
import { addHouseholderVisitButtonId as id } from './AddHouseholderVisitButton.ids';

interface Props {
householdId: string;
}

export const AddHouseholderVisitButton = ({ householdId }: Props) => {
const t = messages.householder.visits;

return (
<Button
variant="outline"
size="sm"
{...tid(id)}
onClick={() => openAddHouseholderVisitModal({ householdId })}
leftIcon={<PlusIcon size={16} />}
>
{t.addVisit}
</Button>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AddHouseholderVisitButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const addHouseholderVisitFormIds = {
nameInput: 'visit-name',
form: 'add-householder-visit-form',
dateInput: 'project-document-date',
descriptionInput: 'project-description-input',
submitBtn: 'submit-button',
notification: {
success: 'add-householder-visit-success-notification',
failure: 'add-householder-visit-failure-notification',
},
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { useCreateVisitMutation } from '@camp/data-layer';
import { debug } from '@camp/debug';
import {
ControlledDateInput,
ControlledFileUpload,
showNotification,
} from '@camp/design';
import type { StorageFile } from '@camp/domain';
import {
createResolver,
documentFileValidator,
documentSchema,
} from '@camp/domain';
import { fileStorageClient } from '@camp/file-storage-client';
import { messages } from '@camp/messages';
import { tid } from '@camp/test';
import { isNull } from '@fullstacksjs/toolbox';
import { Button, createStyles, Group, Stack, TextInput } from '@mantine/core';
import { useForm } from 'react-hook-form';

import { addHouseholderVisitFormIds as ids } from './AddHouseholderVisitForm.ids';

interface FormSchema {
name: string;
date: Date;
description?: string;
documents: StorageFile[];
}

export interface AddHouseholderVisitFormProps {
dismiss: () => void;
householdId: string;
}

const resolver = createResolver<FormSchema>({
name: documentSchema.name(),
date: documentSchema.date(),
description: documentSchema.description(),
documents: documentSchema.documents(),
});

const useStyle = createStyles(theme => ({
label: {
label: {
color: theme.colors.fg[6],
},
},
}));

export const AddHouseholderVisitForm = ({
dismiss,
householdId,
}: AddHouseholderVisitFormProps) => {
const t = messages.householder.visits;
const tt = t.form;
const { handleSubmit, register, formState, control } = useForm<FormSchema>({
resolver,
mode: 'onChange',
});
const [createVisit, { loading }] = useCreateVisitMutation();

const onSubmit = handleSubmit(async inputs => {
try {
const { data } = await createVisit({
variables: { ...inputs, householdId },
});
const visit = data.visit!;
if (isNull(visit)) throw Error('Assert: Visit is null');

showNotification({
title: t.addVisit,
message: messages.projects.notification.successfulCreate(inputs.name),
type: 'success',
...tid(ids.notification.success),
});
dismiss();
} catch (err) {
debug.error(err);
showNotification({
title: t.addVisit,
message: messages.projects.notification.failedCreate(inputs.name),
type: 'failure',
...tid(ids.notification.failure),
});
}
});

const { classes } = useStyle();
return (
<form onSubmit={onSubmit} {...tid(ids.form)}>
<Stack spacing={40}>
<TextInput
data-autoFocus
required
placeholder={tt.nameInput.placeholder}
label={tt.nameInput.label}
size="sm"
error={formState.errors.name?.message}
wrapperProps={tid(ids.nameInput)}
{...register('name')}
/>
<ControlledDateInput
name="date"
required
control={control}
className={classes.label}
wrapperProps={tid(ids.dateInput)}
label={tt.dateInput.label}
placeholder={tt.dateInput.placeholder}
error={formState.errors.date?.message}
/>
<TextInput
wrapperProps={tid(ids.descriptionInput)}
{...register('description')}
label={tt.descriptionInput.label}
placeholder={tt.descriptionInput.placeholder}
error={formState.errors.description?.message}
/>
<ControlledFileUpload
control={control}
name="documents"
defaultValue={[]}
required
label={tt.documentsInput.label}
helper={tt.documentsInput.maxSize}
upload={fileStorageClient.upload}
unUpload={fileStorageClient.unUpload}
filter={(files): File[] => {
const res = files.map(f => {
const parsed = documentFileValidator.safeParse(f);
return {
file: f,
error: !parsed.success ? parsed.error : undefined,
};
});

const firstError = res.find(r => r.error);

if (firstError != null)
showNotification({
message: firstError.error!.issues[0]!.message,
type: 'failure',
});
return res.filter(r => !r.error).map(r => r.file);
}}
/>

<Group spacing={10} position="right">
<Button
size="sm"
variant="filled"
color="secondary"
loading={loading}
onClick={dismiss}
>
{messages.actions.dismiss}
</Button>
<Button
type="submit"
size="sm"
disabled={!formState.isValid}
loading={loading}
{...tid(ids.submitBtn)}
>
{tt.submitBtn}
</Button>
</Group>
</Stack>
</form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AddHouseholderVisitForm';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const addHouseholderVisitModalId = 'add-householder-visit-modal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ModalsProvider } from '@mantine/modals';
import type { Meta, StoryObj } from '@storybook/react';
import { useEffect } from 'react';

import {
AddHouseholderVisitModal,
openAddHouseholderVisitModal,
} from './AddHouseholderVisitModal';

export default {
argTypes: {
opened: {
defaultValue: true,
type: 'boolean',
description: 'Mounts modal if true',
},
},
component: AddHouseholderVisitModal,
decorators: [
Story => (
<ModalsProvider>
<Story />
</ModalsProvider>
),
],
chromatic: { delay: 500 },
} as Meta<typeof AddHouseholderVisitModal>;

type Story = StoryObj<typeof AddHouseholderVisitModal>;

export const Default: Story = {
render: () => {
useEffect(() => {
openAddHouseholderVisitModal({ householdId: 'null' });
}, []);

return <></>;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { messages } from '@camp/messages';
import { tid } from '@camp/test';
import { closeModal, openModal } from '@mantine/modals';

import type { AddHouseholderVisitFormProps } from '../AddHouseholderVisitForm';
import { AddHouseholderVisitForm } from '../AddHouseholderVisitForm';
import { addHouseholderVisitModalId as id } from './AddHouseholderVisitModal.ids';

type Props = Omit<AddHouseholderVisitFormProps, 'dismiss'>;

export const AddHouseholderVisitModal = (props: Props) => (
<AddHouseholderVisitForm {...props} dismiss={() => closeModal(id)} />
);

export const openAddHouseholderVisitModal = (props: Props) =>
openModal({
modalId: id,
children: <AddHouseholderVisitModal {...props} />,
title: messages.householder.visits.title,
size: '490',
padding: '30px',
centered: true,
...tid(id),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AddHouseholderVisitModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AddHouseholderVisitButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const deleteVisitModalIds = {
modal: 'delete-visit-modal',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { showModal } from '@camp/design';
import { messages } from '@camp/messages';

import { deleteVisitModalIds as Ids } from './DeleteVisitModal.ids';

const t = messages.householder.visits.delete.modal;

interface Props {
name: string;
onDeleteVisit: () => Promise<void>;
}

export const openDeleteVisitModal = ({ name, onDeleteVisit }: Props) =>
showModal({
id: Ids.modal,
title: t.title,
children: t.children(name),
cancelLable: t.cancel,
confirmLabel: t.confirm,
size: 'sm',
onConfirm: () => void onDeleteVisit(),
destructive: true,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './DeleteVisitModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const HouseholderVisitsFailureNotification =
'householder-visits-list-failure-notification';
export const HouseholderVisitsTableId = 'householder-visits-list-table';
Loading

0 comments on commit 2c1c539

Please sign in to comment.