mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
feat: add view transfer/payments on invoices
This commit is contained in:
parent
1f259524b9
commit
bea9d86b91
@ -7,14 +7,14 @@ import {
|
||||
DefaultMap,
|
||||
FiltersMap,
|
||||
FormulaMap,
|
||||
HiddenMap,
|
||||
HiddenMap
|
||||
} from 'fyo/model/types';
|
||||
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import {
|
||||
getExchangeRate,
|
||||
getInvoiceActions,
|
||||
getNumberSeries,
|
||||
getNumberSeries
|
||||
} from 'models/helpers';
|
||||
import { InventorySettings } from 'models/inventory/InventorySettings';
|
||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||
@ -22,7 +22,10 @@ import { Transactional } from 'models/Transactional/Transactional';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { FieldTypeEnum, Schema } from 'schemas/types';
|
||||
import { getIsNullOrUndef, safeParseFloat } from 'utils';
|
||||
import {
|
||||
getIsNullOrUndef, joinMapLists,
|
||||
safeParseFloat
|
||||
} from 'utils';
|
||||
import { Defaults } from '../Defaults/Defaults';
|
||||
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
|
||||
import { Item } from '../Item/Item';
|
||||
@ -79,6 +82,22 @@ export abstract class Invoice extends Transactional {
|
||||
: ModelNameEnum.PurchaseReceipt;
|
||||
}
|
||||
|
||||
get hasLinkedTransfers() {
|
||||
if (!this.submitted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.getStockTransferred() > 0;
|
||||
}
|
||||
|
||||
get hasLinkedPayments() {
|
||||
if (!this.submitted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this.baseGrandTotal?.eq(this.outstandingAmount!);
|
||||
}
|
||||
|
||||
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
|
||||
super(schema, data, fyo);
|
||||
this._setGetCurrencies();
|
||||
@ -369,6 +388,14 @@ export abstract class Invoice extends Transactional {
|
||||
},
|
||||
};
|
||||
|
||||
getStockTransferred() {
|
||||
return (this.items ?? []).reduce(
|
||||
(acc, item) =>
|
||||
(item.quantity ?? 0) - (item.stockNotTransferred ?? 0) + acc,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
getStockNotTransferred() {
|
||||
return (this.items ?? []).reduce(
|
||||
(acc, item) => (item.stockNotTransferred ?? 0) + acc,
|
||||
@ -597,4 +624,78 @@ export abstract class Invoice extends Transactional {
|
||||
})) as { name: string }[];
|
||||
return transfers;
|
||||
}
|
||||
|
||||
async getLinkedPayments() {
|
||||
if (!this.hasLinkedPayments) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const paymentFors = (await this.fyo.db.getAllRaw('PaymentFor', {
|
||||
fields: ['parent', 'amount'],
|
||||
filters: { referenceName: this.name!, referenceType: this.schemaName },
|
||||
})) as { parent: string; amount: string }[];
|
||||
|
||||
const payments = (await this.fyo.db.getAllRaw('Payment', {
|
||||
fields: ['name', 'date', 'submitted', 'cancelled'],
|
||||
filters: { name: ['in', paymentFors.map((p) => p.parent)] },
|
||||
})) as {
|
||||
name: string;
|
||||
date: string;
|
||||
submitted: number;
|
||||
cancelled: number;
|
||||
}[];
|
||||
|
||||
return joinMapLists(payments, paymentFors, 'name', 'parent')
|
||||
.map((j) => ({
|
||||
name: j.name,
|
||||
date: new Date(j.date),
|
||||
submitted: !!j.submitted,
|
||||
cancelled: !!j.cancelled,
|
||||
amount: this.fyo.pesa(j.amount),
|
||||
}))
|
||||
.sort((a, b) => a.date.valueOf() - b.date.valueOf());
|
||||
}
|
||||
|
||||
async getLinkedStockTransfers() {
|
||||
if (!this.hasLinkedTransfers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const schemaName = this.stockTransferSchemaName;
|
||||
const transfers = (await this.fyo.db.getAllRaw(schemaName, {
|
||||
fields: ['name', 'date', 'submitted', 'cancelled'],
|
||||
filters: { backReference: this.name! },
|
||||
})) as {
|
||||
name: string;
|
||||
date: string;
|
||||
submitted: number;
|
||||
cancelled: number;
|
||||
}[];
|
||||
|
||||
const itemSchemaName = schemaName + 'Item';
|
||||
const transferItems = (await this.fyo.db.getAllRaw(itemSchemaName, {
|
||||
fields: ['parent', 'quantity', 'location', 'amount'],
|
||||
filters: {
|
||||
parent: ['in', transfers.map((t) => t.name)],
|
||||
item: ['in', this.items!.map((i) => i.item!)],
|
||||
},
|
||||
})) as {
|
||||
parent: string;
|
||||
quantity: number;
|
||||
location: string;
|
||||
amount: string;
|
||||
}[];
|
||||
|
||||
return joinMapLists(transfers, transferItems, 'name', 'parent')
|
||||
.map((j) => ({
|
||||
name: j.name,
|
||||
date: new Date(j.date),
|
||||
submitted: !!j.submitted,
|
||||
cancelled: !!j.cancelled,
|
||||
amount: this.fyo.pesa(j.amount),
|
||||
location: j.location,
|
||||
quantity: j.quantity,
|
||||
}))
|
||||
.sort((a, b) => a.date.valueOf() - b.date.valueOf());
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,7 @@ import { DateTime } from 'luxon';
|
||||
import {
|
||||
getDocStatus,
|
||||
getLedgerLinkAction,
|
||||
getNumberSeries,
|
||||
getStatusMap,
|
||||
getNumberSeries, getStatusText,
|
||||
statusColor
|
||||
} from 'models/helpers';
|
||||
import { Transactional } from 'models/Transactional/Transactional';
|
||||
@ -64,7 +63,7 @@ export class JournalEntry extends Transactional {
|
||||
render(doc) {
|
||||
const status = getDocStatus(doc);
|
||||
const color = statusColor[status] ?? 'gray';
|
||||
const label = getStatusMap()[status];
|
||||
const label = getStatusText(status);
|
||||
|
||||
return {
|
||||
template: `<Badge class="text-xs" color="${color}">${label}</Badge>`,
|
||||
|
@ -99,8 +99,6 @@ export function getLedgerLinkAction(
|
||||
}
|
||||
|
||||
export function getTransactionStatusColumn(): ColumnConfig {
|
||||
const statusMap = getStatusMap();
|
||||
|
||||
return {
|
||||
label: t`Status`,
|
||||
fieldname: 'status',
|
||||
@ -108,7 +106,7 @@ export function getTransactionStatusColumn(): ColumnConfig {
|
||||
render(doc) {
|
||||
const status = getDocStatus(doc) as InvoiceStatus;
|
||||
const color = statusColor[status];
|
||||
const label = statusMap[status];
|
||||
const label = getStatusText(status);
|
||||
|
||||
return {
|
||||
template: `<Badge class="text-xs" color="${color}">${label}</Badge>`,
|
||||
@ -131,17 +129,25 @@ export const statusColor: Record<
|
||||
Cancelled: 'red',
|
||||
};
|
||||
|
||||
export function getStatusMap(): Record<DocStatus | InvoiceStatus, string> {
|
||||
return {
|
||||
'': '',
|
||||
Draft: t`Draft`,
|
||||
Unpaid: t`Unpaid`,
|
||||
Paid: t`Paid`,
|
||||
Saved: t`Saved`,
|
||||
NotSaved: t`Not Saved`,
|
||||
Submitted: t`Submitted`,
|
||||
Cancelled: t`Cancelled`,
|
||||
};
|
||||
export function getStatusText(status: DocStatus | InvoiceStatus): string {
|
||||
switch (status) {
|
||||
case 'Draft':
|
||||
return t`Draft`;
|
||||
case 'Saved':
|
||||
return t`Saved`;
|
||||
case 'NotSaved':
|
||||
return t`NotSaved`;
|
||||
case 'Submitted':
|
||||
return t`Submitted`;
|
||||
case 'Cancelled':
|
||||
return t`Cancelled`;
|
||||
case 'Paid':
|
||||
return t`Paid`;
|
||||
case 'Unpaid':
|
||||
return t`Unpaid`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function getDocStatus(
|
||||
@ -296,7 +302,7 @@ export function getDocStatusListColumn(): ColumnConfig {
|
||||
render(doc) {
|
||||
const status = getDocStatus(doc);
|
||||
const color = statusColor[status] ?? 'gray';
|
||||
const label = getStatusMap()[status];
|
||||
const label = getStatusText(status);
|
||||
|
||||
return {
|
||||
template: `<Badge class="text-xs" color="${color}">${label}</Badge>`,
|
||||
|
@ -1,22 +1,25 @@
|
||||
<template>
|
||||
<Badge class="text-sm flex-center px-3 ml-2" :color="color" v-if="status">{{
|
||||
statusLabel
|
||||
}}</Badge>
|
||||
<Badge
|
||||
class="flex-center"
|
||||
:color="color"
|
||||
v-if="status"
|
||||
:class="defaultSize ? 'text-sm px-3' : ''"
|
||||
>{{ statusLabel }}</Badge
|
||||
>
|
||||
</template>
|
||||
<script>
|
||||
import { getStatusMap, statusColor } from 'models/helpers';
|
||||
import { getStatusText, statusColor } from 'models/helpers';
|
||||
import Badge from './Badge.vue';
|
||||
|
||||
export default {
|
||||
name: 'StatusBadge',
|
||||
props: ['status'],
|
||||
props: { status: String, defaultSize: { type: Boolean, default: true } },
|
||||
computed: {
|
||||
color() {
|
||||
return statusColor[this.status];
|
||||
},
|
||||
statusLabel() {
|
||||
const statusMap = getStatusMap();
|
||||
return statusMap[this.status] ?? this.status;
|
||||
return getStatusText(this.status) || this.status;
|
||||
},
|
||||
},
|
||||
components: { Badge },
|
||||
|
90
src/components/Widgets/LinkedEntryWidget.vue
Normal file
90
src/components/Widgets/LinkedEntryWidget.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="w-quick-edit border-l bg-white flex flex-col">
|
||||
<!-- Linked Entry Title -->
|
||||
<div class="flex items-center justify-between px-4 h-row-largest border-b">
|
||||
<Button :icon="true" @click="$emit('close-widget')">
|
||||
<feather-icon name="x" class="w-4 h-4" />
|
||||
</Button>
|
||||
<p class="font-semibold text-xl text-gray-600">
|
||||
{{ linked.title }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Linked Entry Items -->
|
||||
<div
|
||||
v-for="entry in linked.entries"
|
||||
:key="entry.name"
|
||||
class="p-4 border-b flex flex-col hover:bg-gray-50 cursor-pointer"
|
||||
@click="openEntry(entry.name)"
|
||||
>
|
||||
<!-- Name And Status -->
|
||||
<div class="mb-2 flex justify-between items-center">
|
||||
<p class="font-semibold text-gray-900">
|
||||
{{ entry.name }}
|
||||
</p>
|
||||
<StatusBadge
|
||||
:status="getStatus(entry)"
|
||||
:default-size="false"
|
||||
class="px-0 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Date and Amount -->
|
||||
<div class="text-sm flex justify-between items-center">
|
||||
<p>
|
||||
{{ fyo.format(entry.date as Date, 'Date') }}
|
||||
</p>
|
||||
<p>{{ fyo.format(entry.amount as Money, 'Currency') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Quantity and Location -->
|
||||
<div
|
||||
v-if="['Shipment', 'PurchaseReceipt'].includes(linked.schemaName)"
|
||||
class="text-sm flex justify-between items-center mt-1"
|
||||
>
|
||||
<p>
|
||||
{{ entry.location }}
|
||||
</p>
|
||||
<p>
|
||||
{{ t`Qty. ${fyo.format(entry.quantity as number, 'Float')}` }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Money } from 'pesa';
|
||||
import { getEntryRoute } from 'src/router';
|
||||
import { getStatus, routeTo } from 'src/utils/ui';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import Button from '../Button.vue';
|
||||
import StatusBadge from '../StatusBadge.vue';
|
||||
|
||||
interface Linked {
|
||||
schemaName: string;
|
||||
title: string;
|
||||
entries: {
|
||||
name: string;
|
||||
cancelled: boolean;
|
||||
submitted: boolean;
|
||||
[key: string]: unknown;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['close-widget'],
|
||||
props: {
|
||||
linked: { type: Object as PropType<Linked>, required: true },
|
||||
},
|
||||
methods: {
|
||||
getStatus,
|
||||
async openEntry(name: string) {
|
||||
console.log('op', name);
|
||||
const route = getEntryRoute(this.linked.schemaName, name);
|
||||
await routeTo(route);
|
||||
},
|
||||
},
|
||||
components: { Button, StatusBadge },
|
||||
});
|
||||
</script>
|
@ -4,7 +4,7 @@
|
||||
<template #header v-if="doc">
|
||||
<StatusBadge :status="status" />
|
||||
<DropdownWithActions
|
||||
v-for="group of groupedActions()"
|
||||
v-for="group of groupedActions"
|
||||
:key="group.label"
|
||||
:type="group.type"
|
||||
:actions="group.actions"
|
||||
@ -163,8 +163,7 @@ import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import {
|
||||
docsPath,
|
||||
getActionsForDocument,
|
||||
getGroupedActionsForDocument,
|
||||
getGroupedActionsForDoc,
|
||||
routeTo,
|
||||
showMessageDialog,
|
||||
} from 'src/utils/ui';
|
||||
@ -209,6 +208,9 @@ export default {
|
||||
this.chstatus;
|
||||
return getDocStatus(this.doc);
|
||||
},
|
||||
groupedActions() {
|
||||
return getGroupedActionsForDoc(this.doc);
|
||||
},
|
||||
},
|
||||
activated() {
|
||||
docsPath.value = docsPathMap[this.schemaName];
|
||||
@ -248,9 +250,6 @@ export default {
|
||||
this.quickEditDoc = doc;
|
||||
this.quickEditFields = fields;
|
||||
},
|
||||
groupedActions() {
|
||||
return getGroupedActionsForDocument(this.doc);
|
||||
},
|
||||
getField(fieldname) {
|
||||
return fyo.getField(this.schemaName, fieldname);
|
||||
},
|
||||
|
@ -28,7 +28,7 @@
|
||||
<feather-icon name="settings" class="w-4 h-4" />
|
||||
</Button>
|
||||
<DropdownWithActions
|
||||
v-for="group of groupedActions()"
|
||||
v-for="group of groupedActions"
|
||||
:key="group.label"
|
||||
:type="group.type"
|
||||
:actions="group.actions"
|
||||
@ -267,8 +267,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #quickedit v-if="quickEditDoc">
|
||||
<template #quickedit v-if="quickEditDoc || linked">
|
||||
<QuickEditForm
|
||||
v-if="quickEditDoc && !linked"
|
||||
class="w-quick-edit"
|
||||
:name="quickEditDoc.name"
|
||||
:show-name="false"
|
||||
@ -281,6 +282,12 @@
|
||||
:load-on-close="false"
|
||||
@close="toggleQuickEditDoc(null)"
|
||||
/>
|
||||
|
||||
<LinkedEntryWidget
|
||||
v-if="linked && !quickEditDoc"
|
||||
:linked="linked"
|
||||
@close-widget="linked = null"
|
||||
/>
|
||||
</template>
|
||||
</FormContainer>
|
||||
</template>
|
||||
@ -296,11 +303,13 @@ import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
||||
import FormContainer from 'src/components/FormContainer.vue';
|
||||
import FormHeader from 'src/components/FormHeader.vue';
|
||||
import StatusBadge from 'src/components/StatusBadge.vue';
|
||||
import LinkedEntryWidget from 'src/components/Widgets/LinkedEntryWidget.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import {
|
||||
docsPath,
|
||||
getGroupedActionsForDocument,
|
||||
getGroupedActionsForDoc,
|
||||
getStatus,
|
||||
routeTo,
|
||||
showMessageDialog,
|
||||
} from 'src/utils/ui';
|
||||
@ -321,6 +330,7 @@ export default {
|
||||
QuickEditForm,
|
||||
ExchangeRate,
|
||||
FormHeader,
|
||||
LinkedEntryWidget,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
@ -338,12 +348,42 @@ export default {
|
||||
color: null,
|
||||
printSettings: null,
|
||||
companyName: null,
|
||||
linked: null,
|
||||
};
|
||||
},
|
||||
updated() {
|
||||
this.chstatus = !this.chstatus;
|
||||
},
|
||||
computed: {
|
||||
groupedActions() {
|
||||
const actions = getGroupedActionsForDoc(this.doc);
|
||||
const group = this.t`View`;
|
||||
const viewgroup = actions.find((f) => f.group === group);
|
||||
|
||||
if (viewgroup && this.doc?.hasLinkedPayments) {
|
||||
viewgroup.actions.push({
|
||||
label: this.t`Payments`,
|
||||
group,
|
||||
condition: (doc) => doc.hasLinkedPayments,
|
||||
action: async () => this.setlinked(ModelNameEnum.Payment),
|
||||
});
|
||||
}
|
||||
|
||||
if (viewgroup && this.doc?.hasLinkedTransfers) {
|
||||
const label = this.doc.isSales
|
||||
? this.t`Shipments`
|
||||
: this.t`Purchase Receipts`;
|
||||
|
||||
viewgroup.actions.push({
|
||||
label,
|
||||
group,
|
||||
condition: (doc) => doc.hasLinkedTransfers,
|
||||
action: async () => this.setlinked(this.doc.stockTransferSchemaName),
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
},
|
||||
address() {
|
||||
return this.printSettings && this.printSettings.getLink('address');
|
||||
},
|
||||
@ -416,6 +456,26 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
routeTo,
|
||||
async setlinked(schemaName) {
|
||||
let entries = [];
|
||||
let title = '';
|
||||
|
||||
if (schemaName === ModelNameEnum.Payment) {
|
||||
title = this.t`Payments`;
|
||||
entries = await this.doc.getLinkedPayments();
|
||||
} else {
|
||||
title = this.doc.isSales
|
||||
? this.t`Shipments`
|
||||
: this.t`Purchase Receipts`;
|
||||
entries = await this.doc.getLinkedStockTransfers();
|
||||
}
|
||||
|
||||
if (this.quickEditDoc) {
|
||||
this.toggleQuickEditDoc(null);
|
||||
}
|
||||
|
||||
this.linked = { entries, schemaName, title };
|
||||
},
|
||||
toggleInvoiceSettings() {
|
||||
if (!this.schemaName) {
|
||||
return;
|
||||
@ -446,9 +506,6 @@ export default {
|
||||
|
||||
this.quickEditFields = fields;
|
||||
},
|
||||
groupedActions() {
|
||||
return getGroupedActionsForDocument(this.doc);
|
||||
},
|
||||
getField(fieldname) {
|
||||
return fyo.getField(this.schemaName, fieldname);
|
||||
},
|
||||
|
@ -4,7 +4,7 @@
|
||||
<template #header v-if="doc">
|
||||
<StatusBadge :status="status" />
|
||||
<DropdownWithActions
|
||||
v-for="group of groupedActions()"
|
||||
v-for="group of groupedActions"
|
||||
:key="group.label"
|
||||
:type="group.type"
|
||||
:actions="group.actions"
|
||||
@ -150,8 +150,7 @@ import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import {
|
||||
docsPath,
|
||||
getActionsForDocument,
|
||||
getGroupedActionsForDocument,
|
||||
getGroupedActionsForDoc,
|
||||
routeTo,
|
||||
showMessageDialog,
|
||||
} from 'src/utils/ui';
|
||||
@ -222,14 +221,14 @@ export default {
|
||||
}
|
||||
return fyo.format(value, 'Currency');
|
||||
},
|
||||
groupedActions() {
|
||||
return getGroupedActionsForDoc(this.doc);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getField(fieldname) {
|
||||
return fyo.getField(ModelNameEnum.JournalEntry, fieldname);
|
||||
},
|
||||
groupedActions() {
|
||||
return getGroupedActionsForDocument(this.doc);
|
||||
},
|
||||
async sync() {
|
||||
try {
|
||||
await this.doc.sync();
|
||||
|
@ -110,7 +110,7 @@ import StatusBadge from 'src/components/StatusBadge.vue';
|
||||
import TwoColumnForm from 'src/components/TwoColumnForm.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { getQuickEditWidget } from 'src/utils/quickEditWidgets';
|
||||
import { getActionsForDocument, openQuickEdit } from 'src/utils/ui';
|
||||
import { getActionsForDoc, openQuickEdit } from 'src/utils/ui';
|
||||
|
||||
export default {
|
||||
name: 'QuickEditForm',
|
||||
@ -198,7 +198,7 @@ export default {
|
||||
return fieldnames.map((f) => fyo.getField(this.schemaName, f));
|
||||
},
|
||||
actions() {
|
||||
return getActionsForDocument(this.doc);
|
||||
return getActionsForDoc(this.doc);
|
||||
},
|
||||
quickEditWidget() {
|
||||
if (this.doc?.notInserted ?? true) {
|
||||
|
@ -103,7 +103,7 @@ export default defineComponent({
|
||||
acc[ac.group] ??= {
|
||||
group: ac.group,
|
||||
label: ac.label ?? '',
|
||||
type: ac.type,
|
||||
e: ac.type,
|
||||
actions: [],
|
||||
};
|
||||
|
||||
|
@ -156,6 +156,8 @@ export function getEntryRoute(schemaName: string, name: string) {
|
||||
ModelNameEnum.SalesInvoice,
|
||||
ModelNameEnum.PurchaseInvoice,
|
||||
ModelNameEnum.JournalEntry,
|
||||
ModelNameEnum.Shipment,
|
||||
ModelNameEnum.PurchaseReceipt,
|
||||
].includes(schemaName as ModelNameEnum)
|
||||
) {
|
||||
return `/edit/${schemaName}/${name}`;
|
||||
|
@ -257,7 +257,7 @@ export async function cancelDocWithPrompt(doc: Doc) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getActionsForDocument(doc?: Doc): Action[] {
|
||||
export function getActionsForDoc(doc?: Doc): Action[] {
|
||||
if (!doc) return [];
|
||||
|
||||
const actions: Action[] = [
|
||||
@ -279,7 +279,7 @@ export function getActionsForDocument(doc?: Doc): Action[] {
|
||||
});
|
||||
}
|
||||
|
||||
export function getGroupedActionsForDocument(doc?: Doc) {
|
||||
export function getGroupedActionsForDoc(doc?: Doc) {
|
||||
type Group = {
|
||||
group: string;
|
||||
label: string;
|
||||
@ -287,7 +287,7 @@ export function getGroupedActionsForDocument(doc?: Doc) {
|
||||
actions: Action[];
|
||||
};
|
||||
|
||||
const actions = getActionsForDocument(doc);
|
||||
const actions = getActionsForDoc(doc);
|
||||
const actionsMap = actions.reduce((acc, ac) => {
|
||||
if (!ac.group) {
|
||||
ac.group = '';
|
||||
@ -309,7 +309,7 @@ export function getGroupedActionsForDocument(doc?: Doc) {
|
||||
.sort()
|
||||
.map((k) => actionsMap[k]);
|
||||
|
||||
return [grouped, actionsMap['']].flat();
|
||||
return [grouped, actionsMap['']].flat().filter(Boolean);
|
||||
}
|
||||
|
||||
function getCancelAction(doc: Doc): Action {
|
||||
@ -397,3 +397,15 @@ function getDuplicateAction(doc: Doc): Action {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getStatus(entry: { cancelled?: boolean; submitted?: boolean }) {
|
||||
if (entry.cancelled) {
|
||||
return 'Cancelled';
|
||||
}
|
||||
|
||||
if (entry.submitted) {
|
||||
return 'Submitted';
|
||||
}
|
||||
|
||||
return 'Saved';
|
||||
}
|
||||
|
@ -186,3 +186,34 @@ export function safeParseFloat(value: unknown): number {
|
||||
export function safeParseInt(value: unknown): number {
|
||||
return safeParseNumber(value, parseInt);
|
||||
}
|
||||
|
||||
export function joinMapLists<A, B>(
|
||||
listA: A[],
|
||||
listB: B[],
|
||||
keyA: keyof A,
|
||||
keyB: keyof B
|
||||
): (A & B)[] {
|
||||
const mapA = getMapFromList(listA, keyA);
|
||||
const mapB = getMapFromList(listB, keyB);
|
||||
|
||||
const keyListA = listA
|
||||
.map((i) => i[keyA])
|
||||
.filter((k) => (k as unknown as string) in mapB);
|
||||
|
||||
const keyListB = listB
|
||||
.map((i) => i[keyB])
|
||||
.filter((k) => (k as unknown as string) in mapA);
|
||||
|
||||
const keys = new Set([keyListA, keyListB].flat().sort());
|
||||
|
||||
const joint: (A & B)[] = [];
|
||||
for (const k of keys) {
|
||||
const a = mapA[k as unknown as string];
|
||||
const b = mapB[k as unknown as string];
|
||||
const c = { ...a, ...b };
|
||||
|
||||
joint.push(c);
|
||||
}
|
||||
|
||||
return joint;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user