Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Percentage discount on lists #66

Merged
merged 7 commits into from
Oct 26, 2024
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
11 changes: 11 additions & 0 deletions knex/migrations/20240831000000_equipment-list-discount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function up(knex) {
return knex.schema.alterTable('EquipmentList', (table) => {
table.integer('discountPercentage').notNullable().defaultTo(0);
});
}

export function down(knex) {
return knex.schema.alterTable('EquipmentList', (table) => {
table.dropColumn('discountPercentage');
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Props = {
nextId: number;
nextSortIndex: number;
globalSettings: KeyValue[];
equipmentListDiscountPercentage: number;
readonly?: boolean;
};

Expand All @@ -47,6 +48,7 @@ const EditEquipmentListEntryModal: React.FC<Props> = ({
nextSortIndex,
onSave,
globalSettings,
equipmentListDiscountPercentage,
readonly = false,
}: Props) => {
const invoiceAccounts: Account[] = JSON.parse(getGlobalSetting('accounts.availableAccounts', globalSettings, '[]'));
Expand Down Expand Up @@ -214,6 +216,11 @@ const EditEquipmentListEntryModal: React.FC<Props> = ({
price={equipmentListEntryToEditViewModel?.discount}
text={'Rabatt per rad inklusive moms: '}
/>
{equipmentListDiscountPercentage > 0 ? (
<Form.Text className="text-muted">
Rabatt från listan: {equipmentListDiscountPercentage}%.
</Form.Text>
) : null}
</Form.Group>
</Col>
<Col lg={8} xs={6}>
Expand Down
1 change: 1 addition & 0 deletions src/components/bookings/equipmentLists/EquipmentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ const EquipmentListDisplay: React.FC<Props> = ({
getEquipmentListEntryPrices={(x: EquipmentPrice) => getEquipmentListEntryPrices(x, booking.pricePlan)}
equipmentListEntryToEditViewModel={equipmentListEntryToEditViewModel}
setEquipmentListEntryToEditViewModel={setEquipmentListEntryToEditViewModel}
equipmentListDiscountPercentage={list.discountPercentage}
onSave={onEditModalSave}
nextId={getNextEquipmentListEntryId(list)}
nextSortIndex={getNextSortIndex(getEntitiesToDisplay(list))}
Expand Down
35 changes: 35 additions & 0 deletions src/components/bookings/equipmentLists/EquipmentListHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
faRightFromBracket,
faRightToBracket,
faFileDownload,
faPercent,
} from '@fortawesome/free-solid-svg-icons';
import { EquipmentList, EquipmentListEntry, EquipmentListHeading } from '../../../models/interfaces/EquipmentList';
import { toIntOrUndefined, getRentalStatusName } from '../../../lib/utils';
Expand All @@ -37,6 +38,7 @@ import ConfirmModal from '../../utils/ConfirmModal';
import BookingReturnalNoteModal from '../BookingReturnalNoteModal';
import CopyEquipmentListEntriesModal from './CopyEquipmentListEntriesModal';
import EditEquipmentListDatesModal from './EditEquipmentListDatesModal';
import EditTextModal from '../../utils/EditTextModal';

type Props = {
list: EquipmentList;
Expand Down Expand Up @@ -96,6 +98,7 @@ const EquipmentListHeader: React.FC<Props> = ({
const [showResetDatesModal, setShowResetDatesModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const [showEditDatesModal, setShowEditDatesModal] = useState(false);
const [showEditDiscountPercentageModal, setShowEditDiscountPercentageModal] = useState(false);

// Consts to control which date edit components are shown (i.e. interval, dates or both). Note that
// the logic for usage dates and in/out dates are seperated.
Expand Down Expand Up @@ -222,6 +225,32 @@ const EquipmentListHeader: React.FC<Props> = ({
<FontAwesomeIcon icon={faClone} className="mr-1 fa-fw" />
Hämta utrustning från bokning
</Dropdown.Item>
<Dropdown.Item onClick={() => setShowEditDiscountPercentageModal(true)}>
<FontAwesomeIcon icon={faPercent} className="mr-1 fa-fw" /> Redigera rabatt
</Dropdown.Item>
{showEditDiscountPercentageModal ? (
<EditTextModal
text={list.discountPercentage.toString()}
onSubmit={(newDiscountPercentage) => {
saveList({
...list,
discountPercentage: Math.min(
100,
Math.max(0, parseInt(newDiscountPercentage)),
),
});
setShowEditDiscountPercentageModal(false);
}}
hide={() => setShowEditDiscountPercentageModal(false)}
show={showEditDiscountPercentageModal}
modalTitle={'Redigera rabatt'}
modalConfirmText={'Spara'}
modalSize="sm"
textarea={false}
textFieldSuffix="%"
textIsValid={(text) => !isNaN(parseInt(text))}
/>
) : null}
<CopyEquipmentListEntriesModal
show={showImportModal}
onHide={() => setShowImportModal(false)}
Expand Down Expand Up @@ -351,6 +380,12 @@ const EquipmentListHeader: React.FC<Props> = ({
</>
) : null}
{bookingType === BookingType.RENTAL ? <> / {getRentalStatusName(list.rentalStatus)}</> : null}
{list.discountPercentage !== 0 ? (
<>
{' '}
/ <span className="text-danger">{list.discountPercentage}% rabatt</span>
</>
) : null}
</p>
{showIntervalControls ? (
<>
Expand Down
25 changes: 17 additions & 8 deletions src/components/bookings/equipmentLists/EquipmentListTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,19 +357,24 @@ const EquipmentListTable: React.FC<Props> = ({
}

const entry = getEquipmentListEntryFromViewModel(viewModel);
const priceWithoutDiscount = formatCurrency(addVAT(getPrice(entry, getNumberOfDays(list), false)));
const discount = formatCurrency(addVAT(getCalculatedDiscount(entry, getNumberOfDays(list))));
const priceWithDiscount = formatCurrency(addVAT(getPrice(entry, getNumberOfDays(list))));
const priceWithoutDiscount = formatCurrency(
addVAT(getPrice(entry, getNumberOfDays(list), list.discountPercentage, false)),
);
const discount = getCalculatedDiscount(entry, getNumberOfDays(list), list.discountPercentage);
const formattedDiscount = formatCurrency(addVAT(discount));
const priceWithDiscount = formatCurrency(
addVAT(getPrice(entry, getNumberOfDays(list), list.discountPercentage)),
);

return (
<em className={showPricesAsMuted ? 'text-muted' : ''}>
{entry.discount.value > 0 ? (
{discount.value > 0 ? (
<OverlayTrigger
placement="right"
overlay={
<Tooltip id="1">
<p className="mb-0">{priceWithoutDiscount}</p>
<p className="mb-0">-{discount} (rabatt)</p>
<p className="mb-0">-{formattedDiscount} (rabatt)</p>
</Tooltip>
}
>
Expand Down Expand Up @@ -483,7 +488,7 @@ const EquipmentListTable: React.FC<Props> = ({
onClick={() =>
saveListEntry({
...entry,
discount: getPrice(entry, getNumberOfDays(list), false),
discount: getPrice(entry, getNumberOfDays(list), 0, false), // Here we ignore the list percentage discount by setting it to 0
})
}
>
Expand Down Expand Up @@ -632,9 +637,13 @@ const EquipmentListTable: React.FC<Props> = ({
addVAT(
viewModelIsHeading(viewModel)
? getEquipmentListHeadingFromViewModel(viewModel)
.listEntries.map((x) => getPrice(x, getNumberOfDays(list)))
.listEntries.map((x) => getPrice(x, getNumberOfDays(list), list.discountPercentage))
.reduce((a, b) => a.add(b), currency(0))
: getPrice(getEquipmentListEntryFromViewModel(viewModel), getNumberOfDays(list)),
: getPrice(
getEquipmentListEntryFromViewModel(viewModel),
getNumberOfDays(list),
list.discountPercentage,
),
).value,
getContentOverride: EquipmentListEntryTotalPriceDisplayFn,
columnWidth: 90,
Expand Down
27 changes: 18 additions & 9 deletions src/components/utils/EditTextModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Button, Form, Modal } from 'react-bootstrap';
import { Button, Form, InputGroup, Modal } from 'react-bootstrap';

type Props = {
text: string | undefined;
Expand All @@ -10,7 +10,10 @@ type Props = {
modalTitle: string;
modalHelpText?: string;
modalConfirmText: string;
modalSize?: 'sm' | 'lg' | 'xl';
textarea?: boolean;
textFieldSuffix?: string;
textIsValid?: (x: string) => boolean;
};

const EditTextModal: React.FC<Props> = ({
Expand All @@ -22,7 +25,10 @@ const EditTextModal: React.FC<Props> = ({
modalTitle,
modalHelpText,
modalConfirmText,
modalSize = 'lg',
textarea = true,
textFieldSuffix = undefined,
textIsValid = undefined,
}: Props) => {
const [text, setText] = useState(defaultText ?? '');

Expand All @@ -36,7 +42,7 @@ const EditTextModal: React.FC<Props> = ({
onCancelCallback ? onCancelCallback() : null;
};
return (
<Modal show={show} onHide={onCancel} size="lg" backdrop="static">
<Modal show={show} onHide={onCancel} size={modalSize} backdrop="static">
<Modal.Header closeButton>
<Modal.Title>{modalTitle}</Modal.Title>
</Modal.Header>
Expand All @@ -50,20 +56,23 @@ const EditTextModal: React.FC<Props> = ({
onChange={(e) => setText(e.target.value)}
/>
) : (
<Form.Control
type="text"
name="note"
defaultValue={text}
onChange={(e) => setText(e.target.value)}
/>
<InputGroup>
<Form.Control
type="text"
name="note"
defaultValue={text}
onChange={(e) => setText(e.target.value)}
/>
{textFieldSuffix ? <InputGroup.Text>{textFieldSuffix}</InputGroup.Text> : null}
</InputGroup>
)}
<Form.Text className="text-muted">{modalHelpText}</Form.Text>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onCancel}>
Avbryt
</Button>
<Button variant="primary" onClick={() => onSubmit(text)}>
<Button variant="primary" onClick={() => onSubmit(text)} disabled={textIsValid && !textIsValid(text)}>
{modalConfirmText}
</Button>
</Modal.Footer>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Text, StyleSheet } from '@react-pdf/renderer';
import React from 'react';
import { commonStyles } from '../../utils';
import { EquipmentList } from '../../../models/interfaces/EquipmentList';
import { useTextResources } from '../../useTextResources';

const styles = StyleSheet.create({
...commonStyles,
});

type Props = {
list: EquipmentList;
};
export const EquipmentListDiscountInfo: React.FC<Props> = ({ list }: Props) => {
const { t } = useTextResources();

if (list.discountPercentage === 0) {
return null;
}

return (
<Text style={styles.italic}>
{t('common.equipment-list.discountPercentage')}: {list.discountPercentage}%
</Text>
);
};
26 changes: 23 additions & 3 deletions src/document-templates/components/shared/equipmentListInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Booking } from '../../../models/interfaces';
import { EquipmentListDateInfo } from './equipmentListDateInfo';
import { getSortedList } from '../../../lib/sortIndexUtils';
import currency from 'currency.js';
import { EquipmentListDiscountInfo } from './equipmentListDiscountInfo';

const styles = StyleSheet.create({
...commonStyles,
Expand Down Expand Up @@ -56,6 +57,7 @@ export const EquipmentListInfo: React.FC<Props> = ({ list, booking, showPrices }
<View style={styles.marginBottom}>
<Text style={styles.heading}>{list.name}</Text>
<EquipmentListDateInfo list={list} booking={booking} />
<EquipmentListDiscountInfo list={list} />
</View>

<TableRowWithNoBorder>
Expand Down Expand Up @@ -110,6 +112,7 @@ export const EquipmentListInfo: React.FC<Props> = ({ list, booking, showPrices }
pricePerUnit: getEquipmentListHeadingPrice(
heading,
getNumberOfDays(list),
list.discountPercentage,
),
pricePerHour: currency(0),
},
Expand All @@ -121,10 +124,18 @@ export const EquipmentListInfo: React.FC<Props> = ({ list, booking, showPrices }
<TableCellFixedWidth width={80} textAlign="right">
{!isHeading ? (
<Text>
{getCalculatedDiscount(entry, getNumberOfDays(list)).value > 0
{getCalculatedDiscount(
entry,
getNumberOfDays(list),
list.discountPercentage,
).value > 0
? formatCurrency(
addVAT(
getCalculatedDiscount(entry, getNumberOfDays(list)),
getCalculatedDiscount(
entry,
getNumberOfDays(list),
list.discountPercentage,
),
),
)
: ''}
Expand All @@ -134,7 +145,15 @@ export const EquipmentListInfo: React.FC<Props> = ({ list, booking, showPrices }
<TableCellFixedWidth width={80} textAlign="right">
{!isHeading ? (
<Text>
{formatCurrency(addVAT(getPrice(entry, getNumberOfDays(list))))}
{formatCurrency(
addVAT(
getPrice(
entry,
getNumberOfDays(list),
list.discountPercentage,
),
),
)}
</Text>
) : (
<Text>
Expand All @@ -143,6 +162,7 @@ export const EquipmentListInfo: React.FC<Props> = ({ list, booking, showPrices }
getEquipmentListHeadingPrice(
heading,
getNumberOfDays(list),
list.discountPercentage,
),
),
)}
Expand Down
2 changes: 2 additions & 0 deletions src/document-templates/defaultTextResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const defaultTextResources: Record<Language, Record<string, string>> = {
'common.equipment-list.table-header.total-price': 'Belopp',
'common.equipment-list.table-header.total-price-ex-vat': 'Belopp (exkl. moms)',
'common.equipment-list.total': 'Total',
'common.equipment-list.discountPercentage': 'Rabatt',

// Time Estimate list
'common.time-estimate-list.heading': 'Personalkostnader',
Expand Down Expand Up @@ -122,6 +123,7 @@ export const defaultTextResources: Record<Language, Record<string, string>> = {
'common.equipment-list.table-header.total-price': 'Amount',
'common.equipment-list.table-header.total-price-ex-vat': 'Amount (excl. VAT)',
'common.equipment-list.total': 'Total',
'common.equipment-list.discountPercentage': 'Discount',

// Time Estimate list
'common.time-estimate-list.heading': 'Personnel costs',
Expand Down
Loading