diff --git a/.yarn/releases/yarn-1.22.19.cjs b/.yarn/releases/yarn-1.22.19.cjs index bca1f214f..a084ffc5d 100644 --- a/.yarn/releases/yarn-1.22.19.cjs +++ b/.yarn/releases/yarn-1.22.19.cjs @@ -36549,7 +36549,7 @@ module.exports = { escapeQuotes: escapeQuotes, equal: __webpack_require__(272), ucs2length: __webpack_require__(654), - varOccurences: varOccurences, + varoccurrences: varoccurrences, varReplace: varReplace, cleanUpCode: cleanUpCode, finalCleanUpCode: finalCleanUpCode, @@ -36659,7 +36659,7 @@ function escapeQuotes(str) { } -function varOccurences(str, dataVar) { +function varoccurrences(str, dataVar) { dataVar += '[^0-9]'; var matches = str.match(new RegExp(dataVar, 'g')); return matches ? matches.length : 0; @@ -98196,18 +98196,18 @@ class PackageHoister { const visited = new Map(); - const occurences = {}; + const occurrences = {}; - // visitor to be used inside add() to mark occurences of packages + // visitor to be used inside add() to mark occurrences of packages const visitAdd = (pkg, ancestry, pattern) => { - const versions = occurences[pkg.name] = occurences[pkg.name] || {}; + const versions = occurrences[pkg.name] = occurrences[pkg.name] || {}; const version = versions[pkg.version] = versions[pkg.version] || { - occurences: new Set(), + occurrences: new Set(), pattern }; if (ancestry.length) { - version.occurences.add(ancestry[ancestry.length - 1]); + version.occurrences.add(ancestry[ancestry.length - 1]); } }; @@ -98290,7 +98290,7 @@ class PackageHoister { add(pattern, [], []); } - for (var _iterator9 = Object.keys(occurences).sort(), _isArray9 = Array.isArray(_iterator9), _i9 = 0, _iterator9 = _isArray9 ? _iterator9 : _iterator9[Symbol.iterator]();;) { + for (var _iterator9 = Object.keys(occurrences).sort(), _isArray9 = Array.isArray(_iterator9), _i9 = 0, _iterator9 = _isArray9 ? _iterator9 : _iterator9[Symbol.iterator]();;) { var _ref10; if (_isArray9) { @@ -98304,8 +98304,8 @@ class PackageHoister { const packageName = _ref10; - const versionOccurences = occurences[packageName]; - const versions = Object.keys(versionOccurences); + const versionoccurrences = occurrences[packageName]; + const versions = Object.keys(versionoccurrences); if (versions.length === 1) { // only one package type so we'll hoist this to the top anyway @@ -98324,7 +98324,7 @@ class PackageHoister { let mostOccurenceCount; let mostOccurencePattern; - for (var _iterator10 = Object.keys(versionOccurences).sort(), _isArray10 = Array.isArray(_iterator10), _i10 = 0, _iterator10 = _isArray10 ? _iterator10 : _iterator10[Symbol.iterator]();;) { + for (var _iterator10 = Object.keys(versionoccurrences).sort(), _isArray10 = Array.isArray(_iterator10), _i10 = 0, _iterator10 = _isArray10 ? _iterator10 : _iterator10[Symbol.iterator]();;) { var _ref11; if (_isArray10) { @@ -98337,11 +98337,11 @@ class PackageHoister { } const version = _ref11; - var _versionOccurences$ve = versionOccurences[version]; - const occurences = _versionOccurences$ve.occurences, - pattern = _versionOccurences$ve.pattern; + var _versionoccurrences$ve = versionoccurrences[version]; + const occurrences = _versionoccurrences$ve.occurrences, + pattern = _versionoccurrences$ve.pattern; - const occurenceCount = occurences.size; + const occurenceCount = occurrences.size; if (!mostOccurenceCount || occurenceCount > mostOccurenceCount) { mostOccurenceCount = occurenceCount; @@ -112183,7 +112183,7 @@ module.exports = function generate_contains(it, $keyword, $ruleType) { $it.dataPathArr[$dataNxt] = $idx; var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; } else { out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; @@ -112949,7 +112949,7 @@ module.exports = function generate_items(it, $keyword, $ruleType) { $it.dataPathArr[$dataNxt] = $i; var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; } else { out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; @@ -112972,7 +112972,7 @@ module.exports = function generate_items(it, $keyword, $ruleType) { $it.dataPathArr[$dataNxt] = $idx; var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; } else { out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; @@ -112996,7 +112996,7 @@ module.exports = function generate_items(it, $keyword, $ruleType) { $it.dataPathArr[$dataNxt] = $idx; var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; } else { out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; @@ -113498,7 +113498,7 @@ module.exports = function generate_properties(it, $keyword, $ruleType) { $it.dataPathArr[$dataNxt] = $key; var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; } else { out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; @@ -113514,7 +113514,7 @@ module.exports = function generate_properties(it, $keyword, $ruleType) { $it.dataPathArr[$dataNxt] = $key; var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; } else { out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; @@ -113555,7 +113555,7 @@ module.exports = function generate_properties(it, $keyword, $ruleType) { $it.dataPathArr[$dataNxt] = it.util.toQuotedString($propertyKey); var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { $code = it.util.varReplace($code, $nextData, $passData); var $useData = $passData; } else { @@ -113661,7 +113661,7 @@ module.exports = function generate_properties(it, $keyword, $ruleType) { $it.dataPathArr[$dataNxt] = $key; var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; } else { out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; @@ -113707,7 +113707,7 @@ module.exports = function generate_properties(it, $keyword, $ruleType) { $it.dataPathArr[$dataNxt] = $key; var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; } else { out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; @@ -113868,7 +113868,7 @@ module.exports = function generate_propertyNames(it, $keyword, $ruleType) { it.compositeRule = $it.compositeRule = true; var $code = it.validate($it); $it.baseId = $currentBaseId; - if (it.util.varOccurences($code, $nextData) < 2) { + if (it.util.varoccurrences($code, $nextData) < 2) { out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; } else { out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index 10c8a5af2..636b5dcdd 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -27,6 +27,7 @@ import { RemoveMultipleVolunteerTypes1764811878152 } from '../migrations/1764811 import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-RemoveUnusedStatuses'; import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields'; import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData'; +import { AddDonationRecurrenceFields1770080947285 } from '../migrations/1770080947285-AddDonationRecurrenceFields'; import { RemovePantryFromOrders1769316004958 } from '../migrations/1769316004958-RemovePantryFromOrders'; const config = { @@ -68,6 +69,7 @@ const config = { RemoveMultipleVolunteerTypes1764811878152, RemoveUnusedStatuses1764816885341, PopulateDummyData1768501812134, + AddDonationRecurrenceFields1770080947285, RemovePantryFromOrders1769316004958, ], }; diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index 6bcd2a7e8..3adca4f8b 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -12,7 +12,8 @@ import { import { ApiBody } from '@nestjs/swagger'; import { Donation } from './donations.entity'; import { DonationService } from './donations.service'; -import { DonationStatus } from './types'; +import { DonationStatus, RecurrenceEnum } from './types'; +import { CreateDonationDto } from './dtos/create-donation.dto'; @Controller('donations') export class DonationsController { @@ -54,19 +55,25 @@ export class DonationsController { totalItems: { type: 'integer', example: 100 }, totalOz: { type: 'integer', example: 500 }, totalEstimatedValue: { type: 'integer', example: 1000 }, + recurrence: { + type: 'string', + enum: Object.values(RecurrenceEnum), + example: RecurrenceEnum.NONE, + }, + recurrenceFreq: { type: 'integer', example: 1, nullable: true }, + nextDonationDates: { + type: 'array', + items: { type: 'string', format: 'date-time' }, + example: ['2024-07-01T00:00:00Z', '2024-08-01T00:00:00Z'], + nullable: true, + }, + occurrencesRemaining: { type: 'integer', example: 2, nullable: true }, }, }, }) async createDonation( @Body() - body: { - foodManufacturerId: number; - dateDonated: Date; - status: DonationStatus; - totalItems: number; - totalOz: number; - totalEstimatedValue: number; - }, + body: CreateDonationDto, ): Promise { if ( body.status && @@ -74,13 +81,27 @@ export class DonationsController { ) { throw new BadRequestException('Invalid status'); } + // If we got a recurrence, we should have all of these values + // The next donation dates should be a list of dates we will get from the frontend accordingly + if ( + body.recurrence != RecurrenceEnum.NONE && + (!body.recurrenceFreq || + !body.nextDonationDates || + !body.occurrencesRemaining) + ) { + throw new BadRequestException('recurrence details are incomplete'); + } return this.donationService.create( body.foodManufacturerId, body.dateDonated, - body.status ?? DonationStatus.AVAILABLE, + body.status, body.totalItems, body.totalOz, body.totalEstimatedValue, + body.recurrence, + body.recurrenceFreq, + body.nextDonationDates, + body.occurrencesRemaining, ); } diff --git a/apps/backend/src/donations/donations.entity.ts b/apps/backend/src/donations/donations.entity.ts index 1c40a7c01..a4c58388b 100644 --- a/apps/backend/src/donations/donations.entity.ts +++ b/apps/backend/src/donations/donations.entity.ts @@ -7,7 +7,7 @@ import { ManyToOne, } from 'typeorm'; import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; -import { DonationStatus } from './types'; +import { DonationStatus, RecurrenceEnum } from './types'; @Entity('donations') export class Donation { @@ -44,4 +44,27 @@ export class Donation { @Column({ name: 'total_estimated_value', type: 'int', nullable: true }) totalEstimatedValue: number; + + @Column({ + name: 'recurrence', + type: 'enum', + enum: RecurrenceEnum, + enumName: 'donation_recurrence_enum', + default: RecurrenceEnum.NONE, + }) + recurrence!: RecurrenceEnum; + + @Column({ name: 'recurrence_freq', type: 'int', nullable: true }) + recurrenceFreq: number; + + @Column({ + name: 'next_donation_dates', + type: 'timestamptz', + array: true, + nullable: true, + }) + nextDonationDates: Date[]; + + @Column({ name: 'occurrences_remaining', type: 'int', nullable: true }) + occurrencesRemaining: number; } diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index 6afaaee4e..46e9c6476 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -4,7 +4,7 @@ import { Repository } from 'typeorm'; import { Donation } from './donations.entity'; import { validateId } from '../utils/validation.utils'; import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; -import { DonationStatus } from './types'; +import { DonationStatus, RecurrenceEnum } from './types'; @Injectable() export class DonationService { @@ -45,7 +45,11 @@ export class DonationService { totalItems: number, totalOz: number, totalEstimatedValue: number, - ) { + recurrence: RecurrenceEnum, + recurrenceFreq: number | undefined, + nextDonationDates: Date[] | undefined, + occurrencesRemaining: number | undefined, + ): Promise { validateId(foodManufacturerId, 'Food Manufacturer'); const manufacturer = await this.manufacturerRepo.findOne({ where: { foodManufacturerId }, @@ -63,6 +67,10 @@ export class DonationService { totalItems, totalOz, totalEstimatedValue, + recurrence, + recurrenceFreq, + nextDonationDates, + occurrencesRemaining, }); return this.repo.save(donation); diff --git a/apps/backend/src/donations/dtos/create-donation.dto.ts b/apps/backend/src/donations/dtos/create-donation.dto.ts new file mode 100644 index 000000000..a708fb7b5 --- /dev/null +++ b/apps/backend/src/donations/dtos/create-donation.dto.ts @@ -0,0 +1,59 @@ +import { + ArrayNotEmpty, + IsArray, + IsDate, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + Min, +} from 'class-validator'; +import { DonationStatus, RecurrenceEnum } from '../types'; +import { Type } from 'class-transformer'; + +export class CreateDonationDto { + @IsNumber() + @Min(1) + foodManufacturerId: number; + + @Type(() => Date) + @IsDate() + @IsNotEmpty() + dateDonated: Date; + + @IsNotEmpty() + @IsEnum(DonationStatus) + status: DonationStatus; + + @IsNumber() + @Min(1) + totalItems: number; + + @IsNumber({ maxDecimalPlaces: 2 }) + @Min(0.01) + totalOz: number; + + @IsNumber({ maxDecimalPlaces: 2 }) + @Min(0.01) + totalEstimatedValue: number; + + @IsEnum(RecurrenceEnum) + recurrence: RecurrenceEnum; + + @IsNumber() + @IsOptional() + @Min(1) + recurrenceFreq?: number; + + @Type(() => Date) + @IsArray() + @ArrayNotEmpty() + @IsDate({ each: true }) + @IsOptional() + nextDonationDates?: Date[]; + + @IsNumber() + @IsOptional() + @Min(1) + occurrencesRemaining?: number; +} diff --git a/apps/backend/src/donations/types.ts b/apps/backend/src/donations/types.ts index 163879870..cb63fda33 100644 --- a/apps/backend/src/donations/types.ts +++ b/apps/backend/src/donations/types.ts @@ -3,3 +3,10 @@ export enum DonationStatus { FULFILLED = 'fulfilled', MATCHING = 'matching', } + +export enum RecurrenceEnum { + NONE = 'none', + WEEKLY = 'weekly', + MONTHLY = 'monthly', + YEARLY = 'yearly', +} diff --git a/apps/backend/src/migrations/1770080947285-AddDonationRecurrenceFields.ts b/apps/backend/src/migrations/1770080947285-AddDonationRecurrenceFields.ts new file mode 100644 index 000000000..25c286223 --- /dev/null +++ b/apps/backend/src/migrations/1770080947285-AddDonationRecurrenceFields.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDonationRecurrenceFields1770080947285 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TYPE donation_recurrence_enum AS ENUM ( + 'none', + 'weekly', + 'monthly', + 'yearly' + ); + `); + + await queryRunner.query(` + ALTER TABLE donations + ADD COLUMN recurrence donation_recurrence_enum NOT NULL DEFAULT 'none', + ADD COLUMN recurrence_freq INTEGER, + ADD COLUMN next_donation_dates TIMESTAMP WITH TIME ZONE[], + ADD COLUMN occurrences_remaining INTEGER; + `); + + await queryRunner.query(` + ALTER TABLE donations + ADD CONSTRAINT recurrence_fields_not_null CHECK ( + (recurrence = 'none' + AND recurrence_freq IS NULL + AND next_donation_dates IS NULL + AND occurrences_remaining IS NULL) + OR + (recurrence != 'none' + AND recurrence_freq IS NOT NULL + AND next_donation_dates IS NOT NULL + AND occurrences_remaining IS NOT NULL) + ); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE donations + DROP CONSTRAINT recurrence_fields_not_null, + DROP COLUMN recurrence, + DROP COLUMN recurrence_freq, + DROP COLUMN next_donation_dates, + DROP COLUMN occurrences_remaining; + + DROP TYPE donation_recurrence_enum; + `); + } +} diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index 3c46cb858..5a63830da 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -284,7 +284,12 @@ const RequestDetailsModal: React.FC = ({ mx={3} /> - + {item.quantity} diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 12fd07980..e69fe93da 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -103,6 +103,13 @@ export enum DonationStatus { MATCHING = 'matching', } +export enum RecurrenceEnum { + NONE = 'none', + WEEKLY = 'weekly', + MONTHLY = 'monthly', + YEARLY = 'yearly', +} + export interface Donation { donationId: number; dateDonated: string; @@ -111,6 +118,10 @@ export interface Donation { totalOz: number; totalEstimatedValue: number; foodManufacturer?: FoodManufacturer; + recurrence: RecurrenceEnum; + recurrenceFreq?: number; + nextDonationDates?: string[]; + occurrencesRemaining?: number; } export interface DonationItem {