CRM
- 10 Devlogs
- 68 Total hours
Hey guys, how’s going? Soon school is going to start for me, but luckily it’s my last year in high school, so I’m really happy about this.
Lately I had much time to code, so I continued working on this project.
Last devlog was all backend; I said the engine was there and the screens weren’t. This time it’s quite the opposite. I didn’t really touch the backend, I spent the last 5-6 hours writing the frontend.
Here’s the details of what I did:
Document lines the grid is finally in the record detail. You can add lines, set quantity, price, discount and VAT, additionaly with the subtotal / VAT / total block, that updates while you type. When you save, the whole set of lines is sent at once.
Tags I added the tag picker on the record detail, so you can attach and remove tags from a record as you wish. I also made the page that shows every record with a given tag, from all the modules together.
Duplicates The duplicate finder now has its own screen: you choose a module, you get the pairs it thinks are duplicates, and the you see the two records side by side and pick field by field which value to keep before merging them.
Reports There’s a page to build a report with a wizard (rows, columns, metrics, filters) and a preview that shows you the result before saving it.
Dashboard widgets This is the main thing of this cycle. I wrote all six widget types, which are:
- KPI tile
- split KPI tile
- chart
- table
- pivot
- record list
A widget is just a saved report query plus the type of rendering you want, so it runs on the report engine.
Dashboards with tabs You can create a dashboard, add tabs to it and put widgets inside, so you can keep for example sales and support separated from each other.
Sidebar and topbar The sidebar now has the routes for all the new pages, and the topbar has a quick-create button to make a record from anywhere without opening its module first.
Two things are still not nice and I’m aware: in some places the relation fields show the raw id instead of the name of the record, and the totals of the document lines are not formatted with the user preferences yet, so you read 9054.00 instead of 9.054,00 EUR. Both are small and both are on the list to be improved.
Now I’m working on the PDF generator
See you, I hope you enjoy this project
Hey guys, how it’s going? These days I’ve got quite a lot of work to do, since we’re in the middle of harvest season here in Italy, so I don’t have much time to work on the project.
Unfortunately, today it rains, so I’m home, and I can spend some time on the PC.
Lately I’ve been working a lot on the backend.
The first two things I did are documents and document numbering: I created the model for document rows (DocumentLine), with its engine to calculate totals, including subtotals, discounts and VAT. I also added unit tests for the calculator.
I also implemented automatic document numbering, so documents like quotes, invoices and tickets are automatically assigned a number when they’re created.
Then I worked on quote revisions, with a new QuoteRevisionService to keep track of different versions of the same quote.
Another thing I started working on is document conversion. The idea is to be able to convert a quote into an invoice, but this part is still a work in progress.
I also spent quite a bit of time on reports and dashboards:
Report model and the ReportEngine / ’ReportService`ReportSeeder
Dashboard model, with tabs and widgetsOn the frontend side I’ve scaffolded the pieces for dashboards and widgets — the widgets/ and dashboard-tabs/ components exist and the dashboard and report services are written — but the components themselves are still empty shells. The data layer is ready, the screens aren’t written yet. That’s what I’ll pick up next.
Another big thing I worked on is tags and data quality.
I added the tag model and APIs, together with the services and repositories. I also started working on a duplicate finder and record merging system, so duplicate contacts/companies can be detected and merged instead of leaving messy data around.
For PostgreSQL, I also enabled pg_trgm to improve fuzzy searches for duplicate detection.
Finally, I added user preferences for number and date formats, including a live preview so users can immediately see how their chosen formats will look in the app.
I’ve also added icons for the new modules.
As you can probably tell, this was a very backend-heavy cycle. Almost everything I built this time: document lines, numbering, quote revisions, reports, dashboards, tags and duplicate detection lives in the models and the services, and the UI for most of it isn’t there yet. So this time there isn’t much to show in screenshots: the engine is in place, the screens come next. I’d rather get the model right first and build the interface on top of something solid than the other way around.
Hopefully I’ll be able to find some more time to work on it after harvest, so see you in a while.
Hey guys, back on this project. Last time I said the next round was going to be document lines, my quote was “the only part of this CRM that doesn’t fit the dynamic JSON engine”. That’s what this round was, plus the report engine on top of it.
Document Lines: One single table for all four documents: quote, sales order, delivery note, invoice. The line is exactly the samein all of them, the only thing that changes is the document holding it, and a document here is just a Record like any other, so there’s no type constraint on the line at all.
A line can be a product line, a free text line (no amounts, just something to say in the middle of the document) or a subtotal.
Two things get copied at save time: the description and the vat percentage.
The math is its own class, DocumentLineCalculator, and it’s the one piece of this project I wanted testable completely on its own, because it’s the only place where a wrong rounding produces a wrong tax document.
The order is fixed:
Rounding happens after the discount and after the vat, never on the unit price: round the unit price and the error gets multiplied by the quantity. Rounding mode is HALF_UP, which is the commercial rounding used here, not HALF_EVEN. And a flat discount bigger than the line itself gets clamped instead of producing a negative amount, since that’s a typo 100% of the time.
Lines are always saved all together. The endpoint is a PUT that replaces the whole set. It deletes and rewrites instead of diffing row by row, because a document has a few dozen lines at most and the diff would cost more code than it’s worth. The reason for this rule is the totals: they depend on every line, so a partial save would leave the header and the lines disagreeing, even if only for a moment.
The totals (subtotal, vatTotal, total) get written back into the document’s own JSON, plus the header level discount and shipping cost. They live there so they can be list columns you filter and sort on, which is the whole point of having them.
Now I’m working on the dashboard graphs, widget and this kind of things.I hope you like the project.
I’m sorry if I don’t have any screenshot of the working website, it’s because I focused only on the backend, the frontend of this part still needs to be written.
Hey guys, still on this project. This round was all about the stuff that sits on top of the modules, instead of new modules.
Per-user formatting preferences. There’s a new “My preferences” page in the user menu. Every user picks how they want to see numbers and dates: thousands separator, decimal separator, currency (any ISO code, the symbol gets resolved automatically), how many decimals, whether the symbol goes before or after the number, and whether to cut trailing zeros. Same thing for the date format. There’s a live preview box that updates while you touch the selects, so you see 1.234,50 € turn into $ 1,234.5 as you change this. The setting applies everywhere, lists, record details and so on.
Tags. These are cross-module, which is the whole point; a tag can sit on a lead and on a ticket. There are two kinds: shared ones, visible to the whole workspace, and private ones, which only you see. Each one has a color. You can attach and detach them from any record, and there’s a “all records with this tag” view that goes across modules, filtered by your permissions, so if you can see tickets but not opportunities, you only get the tickets.I did not build this as a field type, even though I already have a TAGS one. That one belongs to a single ObjectType with its own fixed options, so it gives you neither the shared catalog nor the cross-module search.
Duplicate merge. You pick a module and it gives you back the candidate pairs, ranked by how similar their titles are (there’s a threshold you can move, default 0.6). Then you get the two records side by side and choose, field by field, which value survives. When you confirm:
Document numbering. Documents now number themselves. It’s configurable per module: prefix, whether to include the year, padding, separator.
Right now it’s on for three of them:
- quotes → 2025-47
- invoices → FT2025-0001
- tickets → TT643
The counter is per module and per year, and it’s locked while it’s being read, so two people creating a quote at the same time can’t get the same number. If you type a number by hand, yours wins, that matters when you’re entering an old document with its original number.
On top of that, quotes have revisions. Revising 2025-13 gives you 2025-13_2, a brand new document that links back to its parent, while the parent moves to the “revised” stage. And you can revise a revision, so 2025-13_2_2 works too, the counter is progressive per parent, not global, which took a bit more care than I expected.
Next up is the big one: document lines. Quote → order → delivery note → invoice, with quantities, discounts and VAT per line. It’s the only part of this CRM that doesn’t fit the dynamic JSON engine everything else is built on, so it gets its own real tables.
See ya in the next devlog, I hope you like the project.
P.S. you’ll see the tag in the next devlog, I still have to implement the frontend
Hey guys, I’m still working on this project, cause there are lots of things to do.Lately I focused on improving and adding things to the ObjectTypes.
As you can see in the two images of the sidebar, I added like 10 fields.
Among them, the most important are
goals, basically contains the possibility of creating a goal, setting an owner, which is responsible for this goal (clearly depends on the situation), the actual goal and the period. Along with that you can set a metric, so if the goal is revenue, opportunities, leads, custom
project_task, basically it contains the instruction for each member, on a certain projectservice_contract, the support, it contains the subject, the company which is referred to, start and end date, the actual status, so active, expiring, expired, terminated, hours spent and so on.Along with these and the other I didn’t cite here, I added 3 object for the sales, where this two are the most important:
supplier, it contains all the specs, so name, vat, email, phone, website, address, and eventual notes.sales_order, which contains the subject, the number of the order, a relation with the company, and a relation with the contact, a relation with the quote (I don’t know if it’s right, in italian it’s called preventivo), the possibility of setting a status, so created, confirmed, shipped, closed, cancelled, the order date, a relation with the payment Method object, and all the financial part, so the subtotal, with maybe the discount, vat, the total and the infos for the shipping, billing and shipping address.I hope to finish this project asap, since it’s taking me so looong.See ya in the next devlogs
Hey guys, I have stopped this project for a bit, but here I am again. I’m currently improving the CRM, mostly the security, which, for me, is one of the most important parts, and adding functionalities.
Lately I focused on the mails. I added my smtp server, and in the backend, I managed all mails. Here is an example, with the invitation email. It’s in italian, but don’t worry, basically it invites you to the CRM, and then you can create your own account, with password. As you can see in the third screenshot, I can manage the invitation link, whether I want to invite an admin, or a simple employee.
Hey guys, this week was also about making the CRM feel more complete. Alongside several new features, I also spent a good amount of time improving security and fixing one particularly deceptive bug.
First of all, I added a new global Calendar page that shows every record with a date in one monthly view.
Events, tasks, reminders and subscription renewals are all displayed together, each with its own color. Before this, the calendar only existed inside each record type, so there wasn’t a way to see everything happening at once. I also integrated Cal.com. The booking page can now be embedded directly inside the CRM, and when someone books a meeting, a webhook automatically creates an Event record. If the meeting is rescheduled, the existing record is updated instead of creating a duplicate, and if it’s cancelled, the event is removed.
To keep the integration secure, the webhook signature is verified using the raw request body instead of reserializing the JSON.
I spent some time looking at features from Pipedrive, HubSpot and Attio, then implemented a few that I really liked. The Kanban board now shows the total value of each column, making it easy to see how much every stage of the pipeline is worth. I also added rotting, which highlights cards that haven’t been updated for more than two weeks so forgotten leads are easier to spot.
Another big addition is public forms. Every record type can now expose a public form that can be shared with a link or embedded into any website. When someone submits it, a new record is created automatically and any related workflows are triggered. The feature also includes a honeypot for bots and per-IP rate limiting. Finally, I added a Trash Bin, allowing users to restore accidentally deleted records with a single click. I also added the ability to assign an owner directly from the record page.
Passwords now follow the same rules everywhere in the application:
The backend enforces the policy, while the frontend shows a live checklist as the user types.
I also added a Change Password page. When a password is changed, every other active session is logged out while the current one stays active.
At one point login stopped working and always returned “Invalid credentials”. It turned out the password wasn’t the problem at all, the backend was stuck in a crash loop.
A new database column was added as “not null” without a default value, so Hibernate couldn’t apply the change to an existing table. The application never finished starting, and the frontend showed a generic login error.
I fixed the database manually, added a default value to the migration, and made a note for the future: every new “not null” column should always have a default value.
After adding all these features, I went through another security pass. The rate limiter now also protects password changes and the Cal.com webhook. I also added a scheduled cleanup job that removes old expired sessions from the database.
Finally, I configured proper security headers, including a CSP. Public forms can still be embedded on external websites, while the rest of the CRM remains protected from being embedded elsewhere.
Stay tuned, cause I still have lots to do :)
I continued working on this wonderful project. The first thing I implemented was the sidebar, designed as a reusable component. You can expand and collapse it to switch between icons only and icons with text.
I also added a top bar, which contains the company name (top left), a search bar that will allow users to scan the entire database and quickly find what they’re looking for. I also added translations (2 languages for now), light and dark mode, notifications, and a profile section with the user’s name.
Additionally, I implemented a simple dashboard with fake data to start shaping the overall user experience.
Stay tuned.
Still working on my project. It’s been really tough so far, I’m still on the backend, which is very hard.
I feel lucky to have AI helping me; it would be really difficult without it. Honestly, it’s hard even with AI, so I lean on it a lot. I don’t think using AI is a negative: as long as you understand the code it writes and can modify, fix, and improve it, you’re fine.
On the backend I’m currently working on the engine underneath, mainly the database relations.
For example, the record table stores its fields generically (as JSON), so users can define whatever fields they want. The catch is that validating the data is on me, since the fields are dynamic, the database can’t enforce their shape, so it all has to be checked in code, mostly in the frontend.
I’ve also started on the frontend: first by dockerizing it, then by adding the login form that exercises my backend.
Started working on a very big project, called CRM. I hope you know what a CRM is, but for who doesn’t, a crm is basically an app, or maybe a dashboard, used by companies to keep track of their clients, employees and all this kind of things. It helps growing a company, with integrated tickets and so on. This app will be built in the following tech stack:
My idea is to use this as a base, and then for maybe companies who needs it, I could like improve this, add requested features, and optimize everything.
My current situation:
The application will be dockerized for best practice.