diff --git a/src/components/ExportWizard.vue b/src/components/ExportWizard.vue index 0d42b0f1..5790c29c 100644 --- a/src/components/ExportWizard.vue +++ b/src/components/ExportWizard.vue @@ -7,7 +7,7 @@
+

-
- - - - -
-
- -
-

- {{ fyo.schemaMap[schemaName]?.label ?? schemaName }} -

-
- -
+
+ +
+

+ {{ fyo.schemaMap[schemaName]?.label ?? schemaName }} +

+
+
+
- -
-

- {{ fyo.schemaMap[efs.target]?.label ?? schemaName }} -

-
- -
+ +
+

+ {{ fyo.schemaMap[efs.target]?.label ?? schemaName }} +

+
+
@@ -96,68 +78,59 @@
-

{{ t`${numEntries} entries` }}

+

+ {{ t`${numSelected} fields selected` }} +

- diff --git a/src/components/Toast.vue b/src/components/Toast.vue index 83fa6613..fad5e8fc 100644 --- a/src/components/Toast.vue +++ b/src/components/Toast.vue @@ -9,7 +9,7 @@ items-center mb-3 w-96 - z-10 + z-30 bg-white rounded-lg " diff --git a/src/pages/ListView/List.vue b/src/pages/ListView/List.vue index 8770ad19..9e2a5fc2 100644 --- a/src/pages/ListView/List.vue +++ b/src/pages/ListView/List.vue @@ -101,7 +101,7 @@ import ListCell from './ListCell'; export default defineComponent({ name: 'List', props: { listConfig: Object, filters: Object, schemaName: String }, - emits: ['makeNewDoc'], + emits: ['makeNewDoc', 'updatedData'], components: { Row, ListCell, @@ -205,6 +205,7 @@ export default defineComponent({ orderBy, }) ).map((d) => ({ ...d, schema: fyo.schemaMap[this.schemaName] })); + this.$emit('updatedData', filters); }, }, }); diff --git a/src/pages/ListView/ListView.vue b/src/pages/ListView/ListView.vue index dac0e4a4..7f9d3496 100644 --- a/src/pages/ListView/ListView.vue +++ b/src/pages/ListView/ListView.vue @@ -25,10 +25,15 @@ :listConfig="listConfig" :filters="filters" class="flex-1 flex h-full" + @updatedData="updatedData" @makeNewDoc="makeNewDoc" /> - +
@@ -59,7 +64,11 @@ export default { ExportWizard, }, data() { - return { listConfig: undefined, openExportModal: !false }; + return { + listConfig: undefined, + openExportModal: false, + listFilters: {}, + }; }, async activated() { if (typeof this.filters === 'object') { @@ -73,6 +82,9 @@ export default { docsPath.value = ''; }, methods: { + updatedData(listFilters) { + this.listFilters = listFilters; + }, async makeNewDoc() { const doc = await fyo.doc.getNewDoc(this.schemaName, this.filters ?? {}); const path = this.getFormPath(doc.name); diff --git a/src/utils/export.ts b/src/utils/export.ts new file mode 100644 index 00000000..46de34d0 --- /dev/null +++ b/src/utils/export.ts @@ -0,0 +1,334 @@ +import { Fyo } from 'fyo'; +import { RawValueMap } from 'fyo/core/types'; +import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types'; +import { generateCSV } from 'utils/csvParser'; +import { GetAllOptions, QueryFilter } from 'utils/db/types'; +import { getMapFromList } from 'utils/index'; +import { ExportField, ExportTableField } from './types'; + +const excludedFieldTypes = [ + FieldTypeEnum.AttachImage, + FieldTypeEnum.Attachment, +]; + +interface CsvHeader { + label: string; + schemaName: string; + fieldname: string; + parentFieldname?: string; +} + +export function getExportFields( + fields: Field[], + exclude: string[] = [] +): ExportField[] { + return fields + .filter((f) => !f.computed && f.label && !exclude.includes(f.fieldname)) + .map((field) => { + const { fieldname, label } = field; + const fieldtype = field.fieldtype as FieldTypeEnum; + return { + fieldname, + fieldtype, + label, + export: !excludedFieldTypes.includes(fieldtype), + }; + }); +} + +export function getExportTableFields( + fields: Field[], + fyo: Fyo +): ExportTableField[] { + return fields + .filter((f) => f.fieldtype === FieldTypeEnum.Table) + .map((f) => { + const target = (f as TargetField).target; + const tableFields = fyo.schemaMap[target]?.fields ?? []; + const exportTableFields = getExportFields(tableFields, ['name']); + + return { + fieldname: f.fieldname, + label: f.label, + target, + fields: exportTableFields, + }; + }) + .filter((f) => !!f.fields.length); +} + +export async function getJsonExportData( + schemaName: string, + fields: ExportField[], + tableFields: ExportTableField[], + limit: number | null, + filters: QueryFilter, + fyo: Fyo +): Promise { + const data = await getExportData( + schemaName, + fields, + tableFields, + limit, + filters, + fyo + ); + convertParentDataToJsonExport(data.parentData, data.childTableData); + return JSON.stringify(data.parentData); +} + +export async function getCsvExportData( + schemaName: string, + fields: ExportField[], + tableFields: ExportTableField[], + limit: number | null, + filters: QueryFilter, + fyo: Fyo +): Promise { + const { childTableData, parentData } = await getExportData( + schemaName, + fields, + tableFields, + limit, + filters, + fyo + ); + /** + * parentNameMap: Record> + */ + const parentNameMap = getParentNameMap(childTableData); + const headers = getCsvHeaders(schemaName, fields, tableFields); + + const rows: RawValue[][] = []; + for (const parentRow of parentData) { + const parentName = parentRow.name as string; + if (!parentName) { + continue; + } + + const baseRowData = headers.parent.map( + (f) => (parentRow[f.fieldname] as RawValue) ?? '' + ); + + const tableFieldRowMap = parentNameMap[parentName]; + if (!tableFieldRowMap || !Object.keys(tableFieldRowMap ?? {}).length) { + rows.push([baseRowData, headers.child.map((_) => '')].flat()); + continue; + } + + for (const tableFieldName in tableFieldRowMap) { + const tableRows = tableFieldRowMap[tableFieldName] ?? []; + + for (const tableRow of tableRows) { + const tableRowData = headers.child.map((f) => { + if (f.parentFieldname !== tableFieldName) { + return ''; + } + + return (tableRow[f.fieldname] as RawValue) ?? ''; + }); + + rows.push([baseRowData, tableRowData].flat()); + } + } + } + + const flatHeaders = [headers.parent, headers.child].flat(); + const labels = flatHeaders.map((f) => f.label); + const keys = flatHeaders.map((f) => `${f.schemaName}.${f.fieldname}`); + + rows.unshift(keys); + rows.unshift(labels); + + return generateCSV(rows); +} + +function getCsvHeaders( + schemaName: string, + fields: ExportField[], + tableFields: ExportTableField[] +) { + const headers = { + parent: [] as CsvHeader[], + child: [] as CsvHeader[], + }; + for (const { label, fieldname, fieldtype, export: shouldExport } of fields) { + if (!shouldExport || fieldtype === FieldTypeEnum.Table) { + continue; + } + + headers.parent.push({ schemaName, label, fieldname }); + } + + for (const tf of tableFields) { + if (!fields.find((f) => f.fieldname === tf.fieldname)?.export) { + continue; + } + + for (const field of tf.fields) { + if (!field.export) { + continue; + } + + headers.child.push({ + schemaName: tf.target, + label: field.label, + fieldname: field.fieldname, + parentFieldname: tf.fieldname, + }); + } + } + + return headers; +} + +function getParentNameMap(childTableData: Record) { + const parentNameMap: Record> = {}; + for (const key in childTableData) { + for (const row of childTableData[key]) { + const parent = row.parent as string; + if (!parent) { + continue; + } + + parentNameMap[parent] ??= {}; + parentNameMap[parent][key] ??= []; + parentNameMap[parent][key].push(row); + } + } + return parentNameMap; +} + +async function getExportData( + schemaName: string, + fields: ExportField[], + tableFields: ExportTableField[], + limit: number | null, + filters: QueryFilter, + fyo: Fyo +) { + const parentData = await getParentData( + schemaName, + filters, + fields, + limit, + fyo + ); + const parentNames = parentData.map((f) => f.name as string).filter(Boolean); + const childTableData = await getAllChildTableData( + tableFields, + fields, + parentNames, + fyo + ); + return { parentData, childTableData }; +} + +function convertParentDataToJsonExport( + parentData: RawValueMap[], + childTableData: Record +) { + /** + * Map from List does not create copies. Map is a + * map of references, hence parentData is altered. + */ + + const nameMap = getMapFromList(parentData, 'name'); + for (const fieldname in childTableData) { + const data = childTableData[fieldname]; + + for (const row of data) { + const parent = row.parent as string | undefined; + if (!parent || !nameMap?.[parent]) { + continue; + } + + nameMap[parent][fieldname] ??= []; + + delete row.parent; + delete row.name; + + (nameMap[parent][fieldname] as RawValueMap[]).push(row); + } + } +} + +async function getParentData( + schemaName: string, + filters: QueryFilter, + fields: ExportField[], + limit: number | null, + fyo: Fyo +) { + const orderBy = !!fields.find((f) => f.fieldname === 'date') + ? 'date' + : 'created'; + const options: GetAllOptions = { filters, orderBy, order: 'desc' }; + if (limit) { + options.limit = limit; + } + + options.fields = fields + .filter((f) => f.export && f.fieldtype !== FieldTypeEnum.Table) + .map((f) => f.fieldname); + if (!options.fields.includes('name')) { + options.fields.unshift('name'); + } + const data = await fyo.db.getAllRaw(schemaName, options); + convertRawPesaToFloat(data, fields); + return data; +} + +async function getAllChildTableData( + tableFields: ExportTableField[], + parentFields: ExportField[], + parentNames: string[], + fyo: Fyo +) { + const childTables: Record = {}; + + // Getting Child Row data + for (const tf of tableFields) { + const f = parentFields.find((f) => f.fieldname === tf.fieldname); + if (!f?.export) { + continue; + } + + childTables[tf.fieldname] = await getChildTableData(tf, parentNames, fyo); + } + + return childTables; +} + +async function getChildTableData( + exportTableField: ExportTableField, + parentNames: string[], + fyo: Fyo +) { + const exportTableFields = exportTableField.fields + .filter((f) => f.export && f.fieldtype !== FieldTypeEnum.Table) + .map((f) => f.fieldname); + if (!exportTableFields.includes('parent')) { + exportTableFields.unshift('parent'); + } + + const data = await fyo.db.getAllRaw(exportTableField.target, { + orderBy: 'idx', + fields: exportTableFields, + filters: { parent: ['in', parentNames] }, + }); + convertRawPesaToFloat(data, exportTableField.fields); + return data; +} + +function convertRawPesaToFloat(data: RawValueMap[], fields: Field[]) { + const currencyFields = fields.filter( + (f) => f.fieldtype === FieldTypeEnum.Currency + ); + + for (const row of data) { + for (const { fieldname } of currencyFields) { + row[fieldname] = parseFloat((row[fieldname] ?? '0') as string); + } + } +} diff --git a/src/utils/types.ts b/src/utils/types.ts index d9809bb1..fb19c4de 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,3 +1,5 @@ +import { FieldTypeEnum } from "schemas/types"; + export interface MessageDialogButton { label: string; action: () => Promise | unknown; @@ -47,3 +49,20 @@ export interface SidebarItem { schemaName?: string; hidden?: () => boolean; } + + +export interface ExportField { + fieldname: string; + fieldtype: FieldTypeEnum; + label: string; + export: boolean; +} + +export interface ExportTableField { + fieldname: string; + label: string; + target: string; + fields: ExportField[]; +} + +export type ExportFormat = 'csv' | 'json'; \ No newline at end of file diff --git a/utils/index.ts b/utils/index.ts index c6f55b4d..9d2dfe6b 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -37,6 +37,10 @@ export function getMapFromList( list: T[], name: K ): Record { + /** + * Do not convert function to use copies of T + * instead of references. + */ const acc: Record = {}; for (const t of list) { const key = t[name];