diff --git a/app/components/TextWithIcon/TextWithIcon.css b/app/components/TextWithIcon/TextWithIcon.css index 600e479652..a376269571 100644 --- a/app/components/TextWithIcon/TextWithIcon.css +++ b/app/components/TextWithIcon/TextWithIcon.css @@ -6,3 +6,7 @@ .textContainer { width: max-content; } + +.overflowWrap { + overflow-wrap: anywhere; +} diff --git a/app/components/TextWithIcon/index.tsx b/app/components/TextWithIcon/index.tsx index 9d05d2ed26..6a666f417a 100644 --- a/app/components/TextWithIcon/index.tsx +++ b/app/components/TextWithIcon/index.tsx @@ -1,4 +1,5 @@ import { Flex, Icon } from '@webkom/lego-bricks'; +import cx from 'classnames'; import Tooltip from '../Tooltip'; import styles from './TextWithIcon.css'; import type { ReactElement, ReactNode } from 'react'; @@ -34,7 +35,11 @@ const TextWithIcon = ({ ); return ( - + {iconRight && (
{content} @@ -45,7 +50,7 @@ const TextWithIcon = ({ ) : ( <>{icon} )} -
{iconRight ? <> : <>{content}}
+ {iconRight ? <> : <>{content}} ); }; diff --git a/app/components/Upload/FileUpload.tsx b/app/components/Upload/FileUpload.tsx index 9832e92e12..dc311f21ba 100644 --- a/app/components/Upload/FileUpload.tsx +++ b/app/components/Upload/FileUpload.tsx @@ -1,72 +1,61 @@ -import { Button } from '@webkom/lego-bricks'; -import { Component } from 'react'; -import { connect } from 'react-redux'; -import { uploadFile } from 'app/actions/FileActions'; +import { Flex, Icon } from '@webkom/lego-bricks'; +import { UploadIcon } from 'lucide-react'; +import { useState, useRef } from 'react'; +import { useAppDispatch } from 'app/store/hooks'; import styles from './FileUpload.css'; -type State = { - pending: boolean; -}; -type Props = { - uploadFile: (arg0: { file: File }) => Promise; +type FileUploadProps = { + uploadFile: (arg0: { file: File }) => Promise<{ + meta: { fileKey: string; fileToken: string }; + }>; onChange: (arg0: string, arg1: string) => void; className?: string; }; -class FileUpload extends Component { - input: HTMLInputElement | null | undefined; - state = { - pending: false, - }; - handleClick = () => { - this.input && this.input.click(); - }; - handleChange = (e) => { - const file = e.target.files[0]; - this.setState({ - pending: true, - }); - this.props - .uploadFile({ - file, - }) - .then(({ meta }) => { - this.setState({ - pending: false, - }); - this.props.onChange(meta.fileKey, meta.fileToken); - }) - .catch((error) => { - this.setState({ - pending: false, - }); - throw error; - }); +const FileUpload = ({ uploadFile, onChange }: FileUploadProps) => { + const [pending, setPending] = useState(false); + const inputRef = useRef(null); + + const handleClick = () => { + inputRef.current && inputRef.current.click(); }; - render() { - return ( -
- - (this.input = ref)} - className={styles.input} - onChange={this.handleChange} - type="file" - accept="application/pdf" - /> -
- ); - } -} + const dispatch = useAppDispatch(); -const mapDispatchToProps = { - uploadFile, + const handleChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setPending(true); + dispatch( + uploadFile({ file }) + .then(({ meta }) => { + setPending(false); + onChange(meta.fileKey, meta.fileToken); + }) + .catch((error) => { + setPending(false); + throw error; + }), + ); + } + }; + + return ( + + } + onClick={handleClick} + /> + + + ); }; -export default connect(null, mapDispatchToProps)(FileUpload); + +export default FileUpload; diff --git a/app/routes/bdb/components/BdbDetail.tsx b/app/routes/bdb/components/BdbDetail.tsx index b0c9db8ffe..d0d7f5d075 100644 --- a/app/routes/bdb/components/BdbDetail.tsx +++ b/app/routes/bdb/components/BdbDetail.tsx @@ -1,6 +1,4 @@ import { - Button, - Card, ConfirmModal, Flex, Icon, @@ -11,27 +9,35 @@ import { PageCover, } from '@webkom/lego-bricks'; import { usePreparedEffect } from '@webkom/react-prepare'; -import cx from 'classnames'; +import { isEmpty } from 'lodash'; import { Trash2 } from 'lucide-react'; import moment from 'moment-timezone'; -import { useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { deleteCompanyContact, + deleteSemesterStatus, editSemesterStatus, fetchAdmin, fetchSemesters, } from 'app/actions/CompanyActions'; import { fetchEvents } from 'app/actions/EventActions'; import { fetchAll as fetchAllJoblistings } from 'app/actions/JoblistingActions'; +import CollapsibleDisplayContent from 'app/components/CollapsibleDisplayContent'; import CommentView from 'app/components/Comments/CommentView'; +import { + ContentMain, + ContentSection, + ContentSidebar, +} from 'app/components/Content'; import EmptyState from 'app/components/EmptyState'; -import InfoBubble from 'app/components/InfoBubble'; import JoblistingItem from 'app/components/JoblistingItem'; import sharedStyles from 'app/components/JoblistingItem/JoblistingItem.css'; +import Table from 'app/components/Table'; +import TextWithIcon from 'app/components/TextWithIcon'; import Time from 'app/components/Time'; import Tooltip from 'app/components/Tooltip'; +import FileUpload from 'app/components/Upload/FileUpload'; import { selectCommentsByIds } from 'app/reducers/comments'; import { selectEventsForCompany, @@ -41,18 +47,81 @@ import { import { selectAllCompanySemesters } from 'app/reducers/companySemesters'; import { selectPaginationNext } from 'app/reducers/selectors'; import { selectUserById } from 'app/reducers/users'; +import SemesterStatus from 'app/routes/bdb/components/SemesterStatus'; +import { + indexToCompanySemester, + semesterToHumanReadable, +} from 'app/routes/bdb/utils'; import { displayNameForEventType } from 'app/routes/events/utils'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; import { EntityType } from 'app/store/models/entities'; import truncateString from 'app/utils/truncateString'; -import { sortByYearThenSemester, getContactStatuses } from '../utils'; -import SemesterStatusDetail from './SemesterStatusDetail'; import styles from './bdb.css'; +import type { EntityId } from '@reduxjs/toolkit'; +import type { ColumnProps } from 'app/components/Table'; import type { TransformedSemesterStatus } from 'app/reducers/companies'; import type { + CompanyContact, CompanySemesterContactStatus, - SemesterStatus, } from 'app/store/models/Company'; +import type { ListEvent } from 'app/store/models/Event'; + +type RenderFileProps = { + semesterStatus: TransformedSemesterStatus; + type: string; + removeFile: ( + type: string, + semesterStatus: TransformedSemesterStatus, + ) => Promise; + addFile: ( + name: string, + token: string, + type: string, + semesterStatus: TransformedSemesterStatus, + ) => Promise; +}; + +export const RenderFile = (props: RenderFileProps) => { + const { semesterStatus, type, removeFile, addFile } = props; + + const name = semesterStatus[type + 'Name']; + + if (semesterStatus[type]) { + return ( + + + {name ? ( + {truncateString(name, 30)} + ) : ( + - + )} + + removeFile(type, semesterStatus)} + closeOnConfirm + > + {({ openConfirmModal }) => ( + } + size={20} + danger + /> + )} + + + ); + } + return ( + + addFile(fileName, fileToken, type, semesterStatus) + } + /> + ); +}; const BdbDetail = () => { const { companyId } = useParams<{ companyId: string }>() as { @@ -61,6 +130,8 @@ const BdbDetail = () => { const company = useAppSelector((state) => selectTransformedAdminCompanyById(state, companyId), ); + const fetchingCompany = useAppSelector((state) => state.companies.fetching); + const showSkeleton = fetchingCompany && isEmpty(company); const eventsQuery = { company: companyId, @@ -77,7 +148,7 @@ const BdbDetail = () => { const companySemesters = useAppSelector(selectAllCompanySemesters); const studentContact = useAppSelector((state) => company?.studentContact !== null - ? selectUserById(state, company?.studentContact) + ? selectUserById(state, company?.studentContact as EntityId | undefined) : undefined, ); const { pagination: eventsPagination } = useAppSelector( @@ -115,59 +186,44 @@ const BdbDetail = () => { ); const navigate = useNavigate(); - - const [eventsToDisplay, setEventsToDisplay] = useState(3); - if (!company || !('semesterStatuses' in company)) { return ; } - const fetchMoreEvents = () => { - dispatch( - fetchEvents({ - query: eventsQuery, - next: true, - }), - ); - }; - - const semesterStatusOnChange = async ( - semesterStatus: TransformedSemesterStatus, - status: CompanySemesterContactStatus, + const editChangedStatuses = async ( + companyId: EntityId, + tableIndex: number, + semesterStatusId: EntityId | undefined, + contactedStatus: CompanySemesterContactStatus[], ) => { - const newStatus = { - ...semesterStatus, - contactedStatus: getContactStatuses( - semesterStatus.contactedStatus, - status, - ), - }; - const companySemester = companySemesters.find( - (companySemester) => - companySemester.year === newStatus.year && - companySemester.semester === newStatus.semester, - ); - - if (!companySemester) { - throw new Error('Could not find company semester'); + if (!semesterStatusId) { + throw new Error('SemesterStatusId is undefined'); } + const startYear = moment().year(); + const startSemester = moment().month() > 6 ? 1 : 0; - const sendableSemester = { - contactedStatus: newStatus.contactedStatus, - semesterStatusId: newStatus.id, - semester: companySemester.id, - companyId: company.id, - }; + const companySemester = indexToCompanySemester( + tableIndex, + startYear, + startSemester, + companySemesters, + ); - await dispatch(editSemesterStatus(sendableSemester)); - navigate(`/bdb/${companyId}/`); + const id = companySemester?.id; + const semesterStatus = { + companyId, + contactedStatus, + semester: id, + }; + return dispatch( + editSemesterStatus({ ...semesterStatus, semesterStatusId }), + ); }; const addFileToSemester = async ( - fileName: string, fileToken: string, type: string, - semesterStatus: SemesterStatus, + semesterStatus, ) => { const sendableSemester = { semesterStatusId: semesterStatus.id, @@ -179,10 +235,7 @@ const BdbDetail = () => { navigate(`/bdb/${companyId}/`); }; - const removeFileFromSemester = async ( - semesterStatus: SemesterStatus, - type: string, - ) => { + const removeFileFromSemester = async (type: string, semesterStatus) => { const sendableSemester = { semesterStatusId: semesterStatus.id, contactedStatus: semesterStatus.contactedStatus, @@ -193,117 +246,213 @@ const BdbDetail = () => { navigate(`/bdb/${companyId}/`); }; - const semesters = (company.semesterStatuses ?? []) - .slice() - .sort(sortByYearThenSemester) - .map((semesterStatus) => ( - - )); - // CompanyContact in reverse order, latest comes first - const companyContacts = - company.companyContacts && - company.companyContacts - .map((contact) => ( - - {contact.name || '-'} - {contact.role || '-'} - {contact.mail || '-'} - {contact.phone || '-'} - - - - - dispatch(deleteCompanyContact(company.id, contact.id)) - } - closeOnConfirm - > - {({ openConfirmModal }) => ( - } - danger - size={20} - /> - )} - - - - - )) - .reverse(); + const title = `BDB: ${company.name}`; + + const companyInfo = [ + { + text: company.website, + icon: 'globe-outline', + link: true, + }, + { + text: company.address, + icon: 'location-outline', + link: false, + }, + { + text: company.phone, + icon: 'call-outline', + link: false, + }, + { + text: company.companyType, + icon: 'briefcase-outline', + link: false, + }, + { + text: company.paymentMail, + icon: 'mail-outline', + link: false, + }, + { + text: studentContact?.fullName || '-', + icon: 'person-outline', + link: studentContact + ? `abakus.no/users/${studentContact.username}` + : false, + }, + ]; - const events = - companyEvents && - companyEvents - .sort((a, b) => moment(b.startTime).diff(a.startTime)) - .slice(0, eventsToDisplay) - .map((event) => ( - - - {event.title} - - {displayNameForEventType(event.eventType)} - -
+ {companyEvents.length > 0 ? ( + ) : ( - + )} - -

Bedriftens jobbannonser

{fetchingJoblistings && !joblistings.length ? ( @@ -511,16 +524,44 @@ const BdbDetail = () => { ) : ( )} -
- {company.contentTarget && ( - - )} - + {company.contentTarget && ( + + )} + + + {showSkeleton + ? companyInfo.map((info, index) => ( + } + /> + )) + : companyInfo.some((info) => info.text) && + companyInfo.map( + (info) => + info.text && ( + {company.name} + ) : ( + info.text + ) + } + /> + ), + )} + + ); }; diff --git a/app/routes/bdb/components/SemesterStatusDetail.tsx b/app/routes/bdb/components/SemesterStatusDetail.tsx deleted file mode 100644 index ef03d5bf01..0000000000 --- a/app/routes/bdb/components/SemesterStatusDetail.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { - ConfirmModal, - Icon, - Flex, - LoadingIndicator, -} from '@webkom/lego-bricks'; -import { Trash2 } from 'lucide-react'; -import { useState } from 'react'; -import { deleteSemesterStatus } from 'app/actions/CompanyActions'; -import FileUpload from 'app/components/Upload/FileUpload'; -import { useAppDispatch } from 'app/store/hooks'; -import truncateString from 'app/utils/truncateString'; -import { - getStatusColor, - selectMostProminentStatus, - semesterCodeToName, -} from '../utils'; -import SemesterStatusContent from './SemesterStatusContent'; -import styles from './bdb.css'; -import type { EntityId } from '@reduxjs/toolkit'; -import type { TransformedSemesterStatus } from 'app/reducers/companies'; -import type { CompanySemesterContactStatus } from 'app/store/models/Company'; - -const FILE_NAME_LENGTH = 30; -type Props = { - semesterStatus: TransformedSemesterStatus; - companyId: EntityId; - editFunction: ( - semesterStatus: TransformedSemesterStatus, - statusString: CompanySemesterContactStatus, - ) => Promise; - addFileToSemester: ( - fileName: string, - fileToken: string, - type: string, - semesterStatus: TransformedSemesterStatus, - ) => Promise; - removeFileFromSemester: ( - semesterStatus: TransformedSemesterStatus, - type: string, - ) => Promise; -}; - -const SemesterStatusDetail = (props: Props) => { - const [editing, setEditing] = useState(false); - - const dispatch = useAppDispatch(); - - const semesterToHumanReadable = () => { - const { year, semester } = props.semesterStatus; - const semesterName = semesterCodeToName(semester); - return `${year} ${semesterName}`; - }; - - const addFile = (fileName: string, fileToken: string, type: string) => { - setEditing(false); - return props.addFileToSemester( - fileName, - fileToken, - type, - props.semesterStatus, - ); - }; - - const removeFile = (type: string) => - props.removeFileFromSemester(props.semesterStatus, type); - - const { semesterStatus, editFunction } = props; - - if (!semesterStatus) return ; - - const humanReadableSemester = semesterToHumanReadable(); - return ( -
- - - - {['contract', 'statistics', 'evaluation'].map((type) => ( - - ))} - - - ); -}; - -export default SemesterStatusDetail; - -type RenderFileProps = { - semesterStatus: TransformedSemesterStatus; - type: string; - removeFile: (type: string) => Promise; - addFile: (name: string, token: string, type: string) => Promise; - editing: boolean; -}; - -const RenderFile = (props: RenderFileProps) => { - const { semesterStatus, type, removeFile, addFile, editing } = props; - - const uploadButton = (type: string) => ( - addFile(fileName, fileToken, type)} - /> - ); - - const fileNameToShow = (name: string, url?: string) => - name ? {truncateString(name, FILE_NAME_LENGTH)} : '-'; - - const fileName = fileNameToShow( - semesterStatus[type + 'Name'], - semesterStatus[type], - ); - const displayDeleteButton = editing && semesterStatus[type]; - const displayUploadButton = editing && !semesterStatus[type]; - - if (displayDeleteButton) { - return ( - - {fileName} - removeFile(type)} - closeOnConfirm - > - {({ openConfirmModal }) => ( - } danger /> - )} - - - ); - } else if (displayUploadButton) { - return uploadButton(type); - } - - return fileName; -}; diff --git a/app/routes/bdb/components/bdb.css b/app/routes/bdb/components/bdb.css index c387555fd6..c9ce53729b 100644 --- a/app/routes/bdb/components/bdb.css +++ b/app/routes/bdb/components/bdb.css @@ -1,36 +1,5 @@ @import url('~app/styles/variables.css'); -.companyList table { - border-collapse: collapse; -} - -.companyList th { - padding: 5px 0; - border-top: 1px solid var(--border-gray); - border-bottom: 1px solid var(--border-gray) !important; -} - -.companyList th, -.companyList td { - padding-left: 7px; - width: 140px; - border-bottom: 1px dashed var(--border-gray); - text-align: left; - display: table-cell; - - &:first-child { - width: 180px; - } - - &:nth-child(5) { - width: 190px; - } - - &:nth-child(6) { - width: 180px; - } -} - .title { float: left; width: 80%; @@ -54,129 +23,10 @@ } } -/* Semesters in detailroute */ - -.detailTable thead th:nth-child(1), -.detailTable tbody td:nth-child(1) { - width: 120px; -} - -.detailTable thead th:nth-child(2), -.detailTable tbody td:nth-child(2) { - width: 210px; -} - -.detailTable thead th:nth-child(3), -.detailTable tbody td:nth-child(3), -.detailTable thead th:nth-child(4), -.detailTable tbody td:nth-child(4), -.detailTable thead th:nth-child(5), -.detailTable tbody td:nth-child(5) { - max-width: 220px; - min-width: 220px; - width: 220px; - word-wrap: break-word; -} - -.detailTable tbody td:nth-child(3), -.detailTable tbody td:nth-child(4), -.detailTable tbody td:nth-child(5) { - line-height: 1.3; - font-size: var(--font-size-sm); - padding: 5px; - - & a { - color: var(--lego-link-color); - transition: color var(--easing-medium); - } - - & a:hover { - color: var(--lego-red-color-hover); - } -} - -.detailTable thead th:nth-child(6), -.detailTable tbody td:nth-child(6) { - width: 30px; -} - -/* company contact in detailroute */ - -.contactTable thead th:nth-child(1), -.contactTable tbody td:nth-child(1) { - width: 250px; -} - -.contactTable thead th:nth-child(2), -.contactTable tbody td:nth-child(2) { - width: 270px; -} - -.contactTable thead th:nth-child(3), -.contactTable tbody td:nth-child(3) { - width: 330px; -} - -.contactTable thead th:nth-child(4), -.contactTable tbody td:nth-child(4) { - width: 180px; -} - -/* Events in company detailroute */ - -.eventsTable thead th:nth-child(1), -.eventsTable tbody td:nth-child(1) { - width: 350px; -} - -.eventsTable thead th:nth-child(2), -.eventsTable tbody td:nth-child(2) { - width: 180px; -} - -.eventsTable thead th:nth-child(3), -.eventsTable tbody td:nth-child(3) { - width: 100px; -} - -.eventsTable thead th:nth-child(4), -.eventsTable tbody td:nth-child(4) { - width: 200px; - line-height: 20px; - padding: 5px; -} - -.eventsTable thead th:nth-child(5), -.eventsTable tbody td:nth-child(5) { - width: 300px; - line-height: 20px; - padding: 5px; -} - .showAllButton { margin-top: var(--spacing-sm); } -.infoBubbles { - display: flex; - text-align: center; - flex-wrap: wrap; - justify-content: space-between; - - @media (--mobile-device) { - justify-content: space-around; - align-items: center; - } -} - -.infoBubbles > div { - margin: 30px 0; - - @media (--mobile-device) { - margin-right: 20px; - } -} - .adminNote { width: 400px; } @@ -198,3 +48,12 @@ .navigateThroughTime svg { color: var(--color-gray-7); } + +.companyInfo { + min-width: 180px; + min-height: 21px; +} + +.mainContent { + gap: var(--spacing-md); +} diff --git a/app/routes/bdb/utils.tsx b/app/routes/bdb/utils.tsx index 8f63a3961f..06725c5c34 100644 --- a/app/routes/bdb/utils.tsx +++ b/app/routes/bdb/utils.tsx @@ -83,6 +83,12 @@ export const semesterNameOf = (index: number) => { }; return indexToSemesterName[index] || 'spring'; }; +export const semesterToHumanReadable = ( + semester: TransformedSemesterStatus, +) => { + const semesterName = semesterCodeToName(semester.semester); + return `${semester.year} ${semesterName}`; +}; export const semesterCodeToName = (code: Semester) => { const codeToName = { spring: 'Vår', diff --git a/app/store/models/Company.ts b/app/store/models/Company.ts index 0ebf0933d6..119fe80d5b 100644 --- a/app/store/models/Company.ts +++ b/app/store/models/Company.ts @@ -159,6 +159,7 @@ export type AdminDetailCompany = Pick< | 'logo' | 'files' | 'companyContacts' + | 'logoPlaceholder' >; export type UnknownCompany = (
{humanReadableSemester} - - editFunction(semesterStatus, statusCode) - } - /> - - - - - setEditing(!editing)} - name="pencil" - edit - size={20} - /> - - dispatch(deleteSemesterStatus(props.companyId, semesterStatus.id)) - } - closeOnConfirm - > - {({ openConfirmModal }) => ( - } - danger - size={20} - /> - )} - - -