So now that we've wrapped up the data table for transactions, I want to add another button here, which is going to be used to upload a CSV file. So let's go ahead and do the following. Let's go inside of our transactions page.psx right here and let's create an enum here at the top. So I'm going to create an enum variants. First variant is going to be a list representing the current state that we see right now.
The list of transactions. And then we're going to have an import variant. Like that. And let's also create a constant initial import results. And that's going to be an object which has data which is an empty array, errors which is an empty array as well and we're also going to have meta which is an object.
This is going to be used for a package that we are going to add. Right, so I'm just preparing that here. Now, inside of the transactions page, let's go ahead and create a use state from React here. So just ensure that you've added use state from react like this and now inside of here let's go ahead and let's add a variant set variant here and let's give it a default of variants dot list and you can go even further and define the exact type to be variants inside there we go so now what I want to do is I want to find I want to go after these if clause right here before the return and if variant is variants import. In that case, we're gonna return a fragment and for now we're just gonna write a div saying this is a screen for import.
So we are going to render a completely different thing if the user selects the import variant. So if I refresh my app, everything should be exactly as it is now. But if I change this to import, this should change right here. There we go. This is a screen for import.
So that's what we want. So let's bring this back to be the list for now. So now let's create a button which is going to go alongside this add new button. So I'm going to call this upload button right here. And we're going to have a property on upload.
For now just an empty method like this and we actually don't have to collapse this like that. So let's go inside of the transactions here and let's create our upload button.tsx like this. And from here I want to go ahead and I want to install a package so we're going to install npm install or bun add react papa parse so that's the package that we are going to use Let's go ahead and do bun run dev again. Let's import the upload icon from Lucid React and let's import from react-papapars use-csv-reader And let's import a Button component from Components UI Button. Let's write the props onUpload.
It's going to give us the results which can be any and a void. And let's export const uploadButton here. Assign the props. And let's destructure the onUpload here. Now inside of here we're going to go ahead and do the following.
So we are going to destructure, whoops not here, but outside of the return you are going to destructure CSV reader, again my apologies, CSV reader from use CSV reader like this. And then in here I'm gonna add a comment to do add a paywall so later when we implement subscriptions and payments we're gonna add a paywall here but not for now so for now what we're gonna do is we're gonna use this CSV reader which we've just exported from here and inside of here we're going to open a method we are going to open another method and the structure immediately get root props inside like that and give it a type of any. Go ahead and immediately return a button component. Let's go ahead and give it some props. So it's gonna have a size of small.
It's going to have a class name of full-width, button-lg-auto and it's going to have get root props executed like this inside we're going to use the upload icon with the class name size for an mr of 2 and we are going to write import like this. So now let's go back in page.tsx and let's add the upload button, export const upload button like this. So upload button from dot slash upload button. There we go. So if I refresh this, I think that we should see our new button here next to add new transaction.
Let's see. There we go. So right here, let me expand. Alright, but we do, it looks like we have to add some modifications so that they are together and not so far apart. So let me just zoom out so I can see what I'm doing here and let's go back inside of the page here.
So let's find where we have this. So this is here in the card header. So what we are going to do is we're going to wrap these two buttons in a div together like this. And we're going to give this a class name flex item center and gap x of 2 there we go so now this is looking better So what you have to obtain next is a CSV statement of a bank account. So I have an example here which you will be able to find in my source code.
So I extracted these from Revolut. So if you're using that you can go ahead and extract your statements here. So you can also pause the screen if you want to write your own. So what's important here? The most important thing is the dates and how they are written.
So if you want to write this yourselves, you can pause the screen and write it. You don't need all of these, right? You can, it will work even with just one, right? Just make sure you have at least two lines here. Make sure that the currency has the same, sorry, that's the amount.
So make sure that the amount has the same format and make sure that the dates have the same format because we are going to work with these formats right here. So you can find this file in my source code. If not, you can write one yourself. And now what we have to do is we have to import that file. So right now if you go ahead and click import you will choose that and you of course save that as a .csv file and you would select that.
So now we have to change the screen when the user uploads a file. So let's go ahead and do this. Let's go ahead and let's actually use this upload, which we are not using anywhere. So I'm going to go ahead and do the following on the CSV reader. We are going to add on upload accepted.
And we're going to pass on upload. And then inside of here, we have the on upload and we can use that to change what we need, right? So let's go ahead and define our on upload here. On on upload is going to accept the results, which will be a type of initial import results so they are going to look like this you can find more about that in the documentation of react papa parse so inside of here the first thing we are going to do is we're going to set the variant to variants.import that's the first thing we are going to do is we're going to set the variant to variants.import. That's the first thing we are going to do.
And now let's use this on upload and let's assign it here on the button. So this on upload will be fired when upload is accepted on the CSV reader. So let's try it out. So now once I upload, this should change. There we go.
This is a screen for import. So that's what we are going to create next. But first of all, let's create another state here for import results. So just below this, I'm going to write import results, set import results, new state, initial import results. So we are going to use this as our default value.
Let me show you how this looks like in one line. There we go. So we're going to go ahead and set the import results here to the new results. Like that. And now we are able to work with that.
So first of all, let's also create a on cancel import here. So that is very simply going to set import results back to initial import results and it's going to set the variant to variants.list in case we want to cancel the import. Great! And now what I want to do here is instead of rendering a variance dot import we are going to render the import card now import card does not exist yet so let's go ahead and create it so inside of the transactions folder here let's create import dash card dot dsx and let's go ahead and import it here, .slash import card. And now there should be no errors and you should see the import card here.
Great! Let's see if we have to give it some props first. So inside of the import card I'm gonna go ahead and create a type props to have data which will be a string, a matrix of strings basically and on cancel will be a very simple void and on submit will accept that data which will be any and return a void so unfortunately the react popup parse package is not strictly typed so it's it's just easier to use any with this but you're gonna see what kind of data that's going to be. And now let's assign these props here and let's use the data on cancel and on submit here like this. Great.
And now we can go back inside of the page here and we can assign all of those things so data will be importResults.data onCancel will be onCancelImport and onSubmit for now will be just an empty method. So if you're wondering how do I know this is data, well, let's try it out. So on upload, let's console log the results. Oops, all right. Let's see the results when we upload.
So I'm going to expand this and I'm going to import a statement and there we go. Results, data, and this is how it looks like. So it's a matrix of strings, right? Let's go ahead and continue developing the import card right here. So now when it has all the necessary elements, let's go ahead and just define a couple of more constants here.
So const date format, which we accept is going to be yyy-mm-dd hh-mm-ss that's going to be the date format we support and the output format which our database accepts is this. Like that. So we define that here immediately. So we know what we are working with. So this is what needs to appear in your CSV files.
Great. And now let's also add required options. So that's going to be the amount field, the date field and the payee. So these three things are required to make a transaction. Great.
And I'm also going to go ahead and just prepare one interface here. Selected columns state. So that's going to be a key string, which will be string or null like this. And without an equals sign. So very generic, you're gonna see why in a second.
All right, so what we have to do now is we have to import everything we need from card. So we can do that here. So how about we just quickly add that, everything we need from card. And now what we can do, and let's also add the button from components button. What we can do is we can just copy from our main page, this div and all the way up to the card content, right?
So we can copy this part and add it in our import card here like this. So now obviously we have a couple of errors here. So first of all, card content will just say, hello, and then we will close card content, and then we will close card, and then we will close the div. We are not going to use the upload button and we don't need the plus. The onClick can just be an empty arrow method and this will actually say cancel like this and this will not say transaction history but import transaction.
There we go, so you can see how this looks now, like that. And now let's go ahead and enable the cancel button. So I'm gonna go ahead and use the on cancel and use it here. So now when you click on cancel, there we go, it resets the entire thing. So if you import this VSA file, it will change this to import transaction like that.
Great. And now what I want to do is I want to create something called an import table. But just before we can do that we have to do something else. So we have to use this data and we have to separate the headers from the body so if you took a look in the console log right you know that the first item in the array let's take a look at it again so you know the first item in the array in data, whoops it's not here, am I still adding the log? Let me just see inside of page here, whoops, inside of page when I upload something I'm still logging.
Alright so let me add the import. Inside of data as you can see the first array is the header The type, product, these things And everything else is the content So that's what we need to separate in the import card here So inside of here I'm going to add const headers to be data the first in the array and then body will be data slice one like that. So now I know what's the headers and I know what is the body. And in here I'm going to prepare a use state from react so just make sure you've added this let me move it to the top here so use state inside of here will be an empty object but we are already going to prepare it to use the selected columns state. And by default is going to have the selected columns and set selected columns.
Like that. All right. And now let's go ahead inside of the card content and let's render import table which again does not exist yet but we are going to create it. So it's going to accept headers, it's going to accept body, it's going to accept selected columns, selected columns, and it's going to select on table head select change which for now can just be an empty arrow function so now we have to go ahead and create the import table component We're going to do that in the transactions folder as well so import table.tsx and we have to import everything we need from components ui table so let's go ahead and import table, table body, table cell, table head, table header, and table row like this and let's define the props here so we accept the headers which are array of strings and the body, which will be a matrix, right? So let's go ahead and define selected columns here to be a record, string any, or it's going to be string or null, my apologies.
This is the correct type. And then we're going to have on table head select the change. So I know you're wondering what this is but you're going to see in a minute this is quite complex to explain but basically we're going to create a type of table which will be able to use these, like these things at the top, the headers, but instead they're going to be select options. So you're going to be able to change these to not be category but you would be able to click here and then select that to be payee for example, right? We need to match the results from the uploaded file into what we expect here.
So basically this method is going to be this value. So column index, which is a number, value, which is a string or null and a void. So that's going to be that, okay. So those are our props. Let's export const import table here.
And let's go ahead and assign the props. Oops, like this. Let's destructure all the props. So headers, body, selected columns and on table head select change. And now inside of here, we are going to return, let's go ahead and scroll this, a div with a class name of roundedMD and border and overflow hidden.
And then we're going to go ahead and add a table, a table row. We're going to add a table header first, and then we're going to wrap this table row inside. We are going to give this table header a class name of BG muted, and the table row itself will iterate over the headers.map and inside of here, we can skip the item and we can just use the index. Go ahead and open that. And for now, you can add just the table head here and render the index inside and use the key as index as well.
For now. And then inside of here, outside of the header, we're gonna add a table body and the table body will iterate over body.mat. So it will get row, which is an array of strings and an index and inside of here we're gonna use a table row with a key of index and inside we're going to iterate over the individual row.maps we're going to get the cell and index and we're going to render the table cell finally so let's go ahead and give it a key of index render the cell and close the table cell. So quite complicated, I know, but I don't think there is an easier way to handle converting a CSV file into something we can accept into our database and allowing the user on the front end to modify that. All right, so let's import the import table from ./.importtable like this and we pass in the headers and the body so I think we should be good to at least see some results here So if I go ahead and import this statement, there we go.
So this is what I wanted to load. You can see how now I can load my exact information here. And now we're going to turn this indexes into component which will be able to select and choose which of the required fields should we map the data for. So we will choose this as the date, we will choose this as the payee, this as the amount. So that's what the goal is with this.
Great! So let's go ahead and develop that next. So again, please just use a proper CSV file. The date needs to be in this format right here. I suggest you simply use the one I have in my repo or the one I have in my repo is from Revolut.
So if you have Revolut, just go ahead and create the statement export from there. And one important thing, right? So after we add these, keep in mind that most of them are going to be in the past, right? So maybe they won't be in the last 30 days. So perhaps when we upload them, you're actually not gonna be able to see them right here in the transactions, right?
So don't get scared if that happens. It's because of our date filter, which only goes back 30 days, Right? So don't get scared that something is wrong. That's what we are going to confirm in the database as well. So let's go back inside of the import table here.
And now we have to fix this. We shouldn't just render the index. Instead, what we should do is we should handle table head select here. It should be a self-closing tag and it's a custom component we are going to create. So column index is going to be the index.
And selected columns are going to be the existing selected columns and onChange will be onTableHeadSelectChange like this and now in order to develop that component we have to do bonnex chat-cnui.latest addSelectComponent so we already have React Select but we don't have a plain old select component. So that's what we just added inside of our components. Now you should see in the UI folder a select component added. Great! So now let's go ahead and let's go inside of the transactions folder and let's create not a folder but a file table head select.tsx right here.
Let's go ahead and let's define the props for it here. Column index which is a number, selected columns which is a record, string, string or null, And we're going to have on change, which is going to be, make sure you use columns here. So the first parameter will be the column index, which is a number. And the second is the value, which can be string or null. So it needs to be null because you need to give the user an option to skip specific fields right.
Now let's define all options here again. Amount, all options here again. Amount, payee, notes, and date. Basically just ensure that they match. Where did we write our, in the import card perhaps?
Okay, so these are the required options and these are the options that we can use, right? So just make sure that there are no typos with the required options here. So amount, date, payee. Payee is with two e's. So make sure you don't misspell any of those.
And in here we have the extra notes, right? Because they are not required but users should be able to select that if they want to. And now let's go ahead and Let's export const table head select. And let's go ahead and return this. Let's assign the props.
Let's destructure everything we need. So column index, selected columns, and on change here. Great, And let's return a div select. Now let's go back inside of the import table component where we have this error and let's import that from .slash table select. So now when I import here, all of them should say select, right?
They're all now using that same component. Great. So Let's go inside of the table head select and let's develop that component. So first of all, we're going to need to add an import to everything from S slash components UI select. That's the first thing.
So that is the select, select content, select item, select trigger and select value here. And we are also going to need to import the CN library from libutils. And now let's go ahead inside of the method and let's first of all define the current selection if it exists. So selected columns and inside of here we're gonna use backdex column underscore column index. So that's how we are going to target our selected columns here.
Let's go ahead and change this to be the select component. Let's give it a value of current selection or an empty string. So this should be current selection, not current select like that. So that's how we are going to track what's currently in this header, right? We have to specify it like this because there is no other non-generic way to do that, right?
And this is how we're going to track it in the onSubmit method by looking on the indexes. And then we are simply going to map everything under that index as whatever we select in this component. So let's do onValueChange here to accept the value and pass in onChange with the column index as the first argument and the value as the second argument. Now inside of here we are going to add the select trigger component and let's give it a class name CN. So we're going to give it a focus ring offset 0, focus ring transparent, outline none, border none, BG transparent and capitalize.
And we're going to change, if we have a current selection, I want to indicate to the user that they already selected for this column by giving it a blue color. And inside, we're gonna add a select value with a placeholder of skip, right? If they don't want to select anything, they can always skip. And now let's add a select content here. And let's add a first select item inside, which will have a value of skip by default and a label of skip inside like that so that's gonna be the hard-coded option and now we're gonna iterate over the options ourselves so options.map we get the option and the index and inside of here we are rendering a dynamic select item component.
So first of all, inside of it, we are going to render the option, right? So do we have the options at all? Let me just see. Where do we, oh, we have the options right here. My apologies, I thought that they were a prop.
They are not. So we have the constant options here. All right, I confused myself for a second here. So we have the options and Now inside of here, we can pass in the key to be index. We can pass in the value to be option.
We can pass in disabled to be disabled. We don't have disabled yet, but we're gonna create it here in a second. And let's go ahead and give it a class name of Capitalize. Capitalize. So when should an option be disabled?
The option should be disabled if it's already used by another column. So if I select that this row is a category I should not be able to select it here again. It should be disabled to show the user they already chose that option. So I'm going to modify this a bit. I'm not going to immediately return here, instead I'm going to open this curly brackets and I'm going to end a curly bracket here and then inside of here I'm going to add a manual return there we go like this so this will resolve the errors what that allows me to do is go here and define the disabled constant so to see if something is disabled We are going to go over object.values of our selected columns and then we can look at .includes option.
If it does include the option we're gonna do and selected columns we have to find our matching column using the column index. We're going to check if that is not equal to the option like this. So quite a comparison here, but it's the only way we can ensure that we already selected that in another select table head select component right. All right let's see if this is working I think this is it it can now render this great we already added it here so let's try it out. So if I go and import this and select this.
There we go, so everything is skipped by default so I should now be able to select date here. Alright, it's not working and it's not working because we didn't implement the function that will actually select this. So let's go ahead and do that next. So for that we have to go back inside of our import card component right here because we passed an empty function for onTableHeadSelectChange. We have to create a method which will manipulate the selected columns here based on the information sent from the TableHeadSelect component.
So let's go inside of the import card right here. And just below our body, let's define const onTableHeadSelectChange. And in this method, the first argument will be the column index which will be a number and then we're going to have a value which can be string or null. And inside of here we are going to modify the setSelectedColumns right here by getting the previous value of them and then we're going to open a method, So don't return an immediate object, just open a method here and we're going to define the new selected columns inside. We're just going to spread the previous ones, right?
Or the current, however you want to call them. And then we're going to do for const key in new selected columns we're going to check if new selected columns key so the existing value matches the new value in that case new selected columns key will be reset to null like this and then inside of here we're going to say if value is skip in that case the value will be null for this next step which is going to be new selected columns and then we're going to write column underscore column index will be value like that and then return new selected columns like that so just be careful with this names so this repeated a couple of times in our project so make sure that it is exactly the same every single time. All right, so it should be here in the current selection and in here in the disabled in the table head select. Great, so now inside of the import card we can use the on table head select change and we can pass it here. There should be no type errors because the types are correct.
Let's try it out again. So if I select date here, there we go. This becomes date. And you can see how it's disabled in all the other options. So now this will be my payee and this will be my amount and I don't have to fill the notes but if I want to I can put the notes here for example and I can always bring this back to skipped and then you can see amount is now freed or if I skip this then payee is freed.
If I skip this then date is freed. So that's what we wanted to achieve. Great! So now what we want to do is we want to create the continue button which will indicate to the user how many of the required fields have they set. Let's go back inside of our import card component and we can actually close everything else.
So now what I wanna do is I wanna define a constant called progress and that's gonna go over object.values, selected columns and it's going to filter by Boolean here. Like this and include dot length, like that. And then what we are going to do is we are going to go and find the cancel button. And here we're going to add a new button, which will say continue. But in the parentheses, we're going to calculate, well, not calculate, but we're just gonna show progress divided by, but not actually divided, just like, for example, three out of five, something like that.
And then inside of here, we're gonna say required options dot length, like this. There we go. So you can see that now it says four of three. Okay, so maybe not the best logic to handle this. So what we can do for now to simplify it is go inside of the table head select component and just remove the notes for now, right?
So make sure you only have these three which are exactly the same as these three in the required options. So that will simplify things for us. And then we can also go ahead and give this button a disabled property. If progress is less than required options.length, like this, and let's go ahead and give it an on click, like that, or do we already have a, we have on submit, but okay, we're gonna handle that later. So let's just see if this is working so if I import a new CSV there we go so continue 0 out of 3 but if I add a date that's 1 out of 3 if I add payee that's 2 out of 3 And finally I add an amount and that's three out of three.
If I reset something to skip, I get back to two out of three. Perfect. That is exactly what we wanted to do. Let's go ahead and let's give this a size of small while we are here. Great.
Now I just want to slightly modify how this looks on mobile, right? This cancel and continue button. So I think that we can do this. We can do flex call and then here we can do LG flex row like that and then we can do gap y2 here. We can give this button a class name of, let's go ahead and just collapse all these items here.
So I just want it to look a bit nicer and more consistent on mobile. So full width on mobile, but on LG, on large is going to be auto like this and then I want to copy this and paste it for the button below there we go, so now when I expand perfect now this looks a bit more consistent not perfect, you know, but consistent And I want to do the same thing here, right? It looks a bit weird. So let's just fix that as well. So let's go inside of page.tsx right here.
So inside of here, we do the same thing. Let me just collapse these two. Let's give this a class name of full width but on LG auto and then let's give this a flex call but an LG flex row and let's give it a gap y of 2. There we go. So now this 2 is also consistent and when I import this is also consistent.
Perfect! So what we have to do now is we have to convert or format these values from well this matrix of strings into something that we can send it to our backend and we know what our backend expects so if we go into transactions API route and if we find the bulk create right here it accepts an array of these objects right here So that's what we're gonna have to do here. So let's go inside of our import card here and now we're gonna go ahead and develop this on click method right here. So that's gonna be our handle continue method. So we're gonna do that just below the progress.
So const handle continue here is going to be an empty arrow function, which will first of all, create a method, get column index, which we're going to reuse here. So we're going to accept column string, and we're going to return column.split underscore and the first one. Basically, you know how we name our column and then we do column index. So now we're gonna have to keep doing this. So I don't want this to write that many times.
So let's just create a little method to help us with that here. And now, well, here comes the bit of a confusing part. I mean, everything was confusing, but this is perhaps the most confusing part so far. So basically we now have to turn this into from this weird array, right? Into our very simple array of objects, right?
And we also have to create each of these objects inside because currently we don't know what's the date, what's the category, what's the amount, none of those things. So first let's create a more structured mapped data here. So it's going to be an object which will have headers. And this headers will go headers.map. And we are going to do underscore header because we don't need it, we just want the index.
And inside of here, we're going to get our column index for this specific header. So what the user selected in the select field will be get column index from our method above. And we're going to pass in the column underscore index like this. And then we're going to return selected columns we're going to find column underscore column index here or null so that's the headers and now for the body we're going to do a similar thing so body.map we get the row in here and first we have to transform the row so const transformedRow is going to be row.map get the cell get the index here let's do the same thing so let's get the column index and let's return selected columns backdicks column underscore column index like this and then if we have that we're going to return cell otherwise we're going to return null so we have to return the value of what's in the cell right but only if we have that selected and matching under this column index from that header. So it's confusing, you have to keep thinking of that matrix.
And then we're just going to do a quick little check here to return transform the row dot every and we're just going to ensure that item is not empty. So if item is now return an empty array like this, otherwise transform the row, because I found that there is a possibility of a bug here. And then we'll just add a final filter here to only show full length rows like that. So very, very confusing, I know, but let's make it easier for us and let's at least console log this so we know what just happened, right? And now let's use this handle continue and let's add it here like this.
So I'm going to expand this and let's see if this will... So make sure you have selected the date, the payee and the amount. Let's click continue and now we have mapped data here. So there we go. You can see now I have headers.
So I skipped the first one, I skipped the second one and only on the third one I selected the date. So now I transformed my data here to only pick those exact things from my CSV file. Do you kind of understand what we did here? Obviously it's still very confusing because you keep having to think about how that looks in a matrix, right? So we only populate and transform the body fields which have the matching headers selected, right?
I know it's complicated, but again, I don't know if there's an easier way to do this, right? It's simply a complicated thing to do. But we are still not done. So now that we have the mapped data, we have to transform it into something our backend can accept. So let's do const array of data here to be mapped data dot body dot map row return row dot reduce get the accumulator which can be any then get the cell and then get the index and inside of here we are going to get the header so header is going to be mapped data dot headers and it will match the same index because we just took a look at the console log so we know that the indexes match now in in case header is not null we are going to add to the accumulator that header and that cell.
So we are going to create a proper object like date equals and then a date string, right? Otherwise, we're just going to return accumulator And let's start with an empty object in the beginning of the reduce method like this. And let's console.log this now, so array of data. So this will now be familiar to something that we can accept in our backend. So if I click continue again there we go.
This is now something that we can send into our backend. Much, much better. Great! But we are not done yet because we have to format this amounts into milli units so let's go ahead and do that so the last formatting method here is going to be our const formatted data array of data dot map gets the item here and we're going to spread everything inside and then we're gonna get the amount to be convert amount to milli units parse float item dot amount So make sure you've imported convert amount to milli units. Let me just add this here or here.
It doesn't matter really and we also have to modify the date because it doesn't match exactly what we need. So let's go ahead and just import format and parse from date FNS so remember we defined this date format and this output format right so your CSV files need to have this date format otherwise it's not going to work. So let's go ahead and do this. Inside of date we are going to format but first we have to parse the existing item.date using the date format constant and then new date my apologies for this popping up, so new date and then lastly add the output format like this there we go and let's console.log our, my apologies, formatted data finally. My VS Code started getting slow over how many code we have here.
So let's finally try this out, continue. And this is the formatted data. So the exact dates that we need, and let's look at the amount. It's converted to million units. So this is something that we can now finally send to the backend.
The only thing that's missing is which account do we make this for? So that's what we are going to do next. But this is ready. We can now do on submit, formatted data. So if your formatted data looks like this, you are ready to send this further.
Great! So what we have to do now is we have to create a dialog which will confirm this formatted data by selecting an account But I want to do that in the next chapter because we are already almost an hour in. So I'll give you some time to take a look at what we just wrote because we wrote a ton of things. Great, great job.