Posted

in

,

Filament PHP as a replacement of Notion for a Personal Management System

Ouch. The challenge seems hard and uncommon, and I don’t know why I allocated ten days to implement a better “personal management system”.

At the start, I planned to allocate three days to build a personal application to track what I eat, and it ended by a full-featured system to manage everything else. It took me two weeks, which is rather acceptable.

Currently, this development “personal management system” contains the following modules:

  • Food – What I eat everyday. I track this down as I am doing bodybuilding since 1 years now.
  • Habits – Tracking my habits has always been part of my process. I didn’t even use Notion for that. For the last two years, I used a mobile app.
  • Health – Tracking some health constants is important to me. I invested to almost all Withings tracking devices. I love Withings, but their app sucks.
  • Journal – I am not an adept of “diaries”. It didn’t succeed consistently in the past. But this time, I feel that I need to put some notes on a calendar, just to remember the date of events.
  • Projects – I am building a lot of stuff. I used many systems to track what I have to do, from Github issues on a private repo, from Notion, Todoist, and other self-hosted projects. It’s time to gather everything in one place!
  • Notes/Markdown/Wiki – Yes, i need a space to take notes.

So the idea was basically to do this migration:

GoalFormer systemNew system
Tracking food<Nothing>Laravel / Filament
HabitsMobile app “Habitudes”Laravel / Filament
HealthWithings mobile appLaravel / Filament
Personal DiaryObsidianLaravel / Filament
ProjectsNotion/Obsidian/TodoistLaravel / Filament
NotesObsidianLaravel / Filament

In this post, I cover all features and technical details about the implementation. If you are a Laravel developer but don’t know yet Filament PHP, it may help you to have a clue on its capacities.

Tracking food

I used two tables: one to record the food in itself (type, brand, nutrients, etc.) and the other to track down what I eat everyday.

The “food tracker” is sorted by group and based on two criteria, as I always want to have a clear vision of my last dishes.

It’s not as trivial because there is no natural order between “breakfast”, “lunch”, “dinner”, etc. So in my table function I added groups like this:

Where FoodMoment is an Enum containing a custom MySQL query to order them correctly from the last dish of the day to the first one:

As it can be boring to enter each food one by one, I created also an Action to duplicate the meal of a particular day.

It’s quite easy to do it. Just set up a header action with the relevant code.

Habits

I wanted to track my habits with a single click. It was a bit challenging, but it works well.

To implement that, I used the plugin Guava Calendar. By defining the methods onDateClick and onEventClick, I can delete or add the underlying record of my HabitLog model.

After each change, you’ll need to call $this->refreshRecords() to live-update the calendar.

To track efficiently habits, you also need to define what is a “success” or a “failure”, based on the desired frequency. I added a migration/model/filament resource to achieve that. No particular difficulty here, except to compute things.

Computing that stuff is not so easy, therefore I decided to implement a scheduled job instead of live-refreshing the dashboard. My current code is probably less than optimal but as it is launched every night, I don’t particularly care. After computation, all habit streaks – which depend of the objectives I have defined – are updated.

Health

I own almost all Withings connected devices. These are great to track down as many things as possible. The issue is that their application sucks. Fortunately, they have a very good free API, so I was able to retrieve the data I want.

By digging into their API, I was even surprised to see the number of stuff recorded by my devices, especially with the sleep analyzer.

I didn’t make widgets for all this data yet. I only implemented the key-indicators (weight and sleep indicators).

However, I record in the database almost everything, leading to 45 tracked indicators:

The main challenge was the Oauth2 authentication part, as it requires a callback URL. As I want to keep my app on my home network, I used Boring Proxy, which is simple and good (automatic SSL support and self-hosted).

To secure the whole stuff, I added an Basic Authentication on NGINX (in addition to the Filament/Laravel auth) with an exception to this URL.

Journal

I used the same calendar plugin as above. It really greats a it provides several views (month/week/day).

It allows also to add user-defined events, with different colors. This is perfect for my usage.

I don’t keep a diary on a daily basis, but I like to track some noticeable events on various subjects (family, health, work events, etc.)

My philosophy here is that with AI, personal data will be king. This is why I think it’s a good bet to record key events. For health notably, I think that everyone should start to track everything, from symptoms to medical exams.

Projects / Todo

In matter of personal organization, I like to have several Kanban, one per project, and another one for daily tasks.

To implement that, I used this Filament plugin. It’s great, but I hacked it a bit to be able to switch projects easily.

Implementing the “Project selector” is done directly by overriding one of the Livewire view of the plugin.

In the mount() method, I load all projects, and I define the $currentProject. Then in the kanban-board.blade.php overriden view, I set Filament tabs.

To ensure that the columns (eg. status) are refreshed correctly, you need to add a “$refresh” in the wire:click event. It took me a bit of time to figure it out.

Of course, a particular project can be closed, so it will not appears in the list of projects on the Kanban board.

Wiki / Notes

A personal management system without a Wiki or notes would be stupid. However, I push it further to “imitate” how Notion works:

  • Unlimited nested pages
  • Block-based
  • User-defined tables
  • Custom view with meta-information

Implementing a tree view of pages

My first task was to implement a tree view of pages. I defined a WikiPage model and migration, with two additional columns: order and parent_id.

Then I used the plugin Filament Tree to implement the tree view.

In my WikiPageResource, I added the tree on top of the table, as a Widget, to have a clear view of the tree.

I preferred this solution rather that implementing either in the main menu (it would be a mess), or a second navigation vertical menu, as it would reduce the space allocated to the table.

Currently, the content of a Wiki Page can contains an unlimited number of blocks:

  • Markdown editor
  • User-defined table
  • Stats widgets

When a Wiki Page is viewed, it displays a meta-block with some information on the page:

  • Creation and modification date
  • Link to parent
  • Link to children

This header is simply an Infolist, not even a custom view.

User-defined tables

I was a bit surprised to see that no plugin exists to let the users to define new tables with a GUI. So I needed to develop it myself.

I made models and resources to allow me to set up new tables without touching the code. This resource allows to define the form fields and columns.

For the form fields, it supports all Filament main fields except the Repeater and the Builder.

For the columns, it supports the following:

The fields and columns definitions are stored through the WikiUserTable model:

Each user table accepts some of the Filament settings, but not all.

The values of each record of the table are not stored as usual. In other words, no migrations are created. There is only one table to keep all values in a JSON column. One record is created per row.

This induces some difficulties for particular aspects, like summarizers and filters. Indeed, these table feature are using directly SQL queries to filter or to summarize rows. As we don’t use a dedicated SQL table for each user-table, this would not work… except if we hack it a bit.

The Select filter for instance needs to be customized to search in JSON data. I implemented it like that:

It’s a hack, but it works.

Showing the form and columns as defined by the user if simply a succession of “if” conditions, nothing sophisticated.

It’s rather a lot of code, but not difficult to maintain. When I have to add a supported option for a form field or for a column, I can do it easily.

Integrating user-defined table in Wiki pages

Several steps are needed here.

First, I needed to define a custom Infolist in my WikiPageResource. This Iinfolist is responsible of displaying the full Wiki Page in view mode.

The definition of this infolist get all blocks of the model, and display the relevant InfoList Entry.

For the user table, we need to define a custom Infolist Entry (which is a Livewire component called UserTable), and we pass the name of the user table to display in the $state.

The custom Infolist component is extremely simple:

It calls the Livewire component WikiUserTable, with the relevant $tableId, so we can load the table conditionally based on this parameter.

The result

We now have full user-defined tables as wanted in our WikiPages, at the position we want.

Doing the same of Stats Widgets

Each Wikipage allows also to add some Stat Widgets. The principle is the same as user-defined table: a custom InfoList entry that calls the relevant component.

The difference is that we don’t have to call a custom Livewire component, but directly the widget.

This widget supports two kinds of values: static and dynamic ones. Static ones are directly defined in the WikiPage form, while dynamic ones are computed from an existing user-table.

For instance, here, we want to get the sum of a column of a user-defined table.

The result is as follows:

The beauty is that this whole page is made through the GUI, not a single line of code.

Conclusion

For now I am happy with this system. Obviously, it’s not as practical as Notion, but at least I have the full control, it’s self-hosted, and if I need a feature I just have to implement it. The possibility to define custom tables within the interface and integrate them to Wiki Pages is largely enough for my needs.

I feel it’s my new home, containing everything I need for my personal organization and tracking.

I identified some potential features that I may develop in the future:

  • Integration of an LLM (Perplexity AI?), that would be useful when I want to document myself in my personal Wiki.
  • Document management: scanning everything and uploading.
  • I also do regular blood tests. I may develop an AI to automatically track and record blood indicators from the results that are sent in PDF.

My overall feeling is that with the increase of AI usage for development, custom tailored solutions will be the way to go. Implementing this personal management system is also a bet: in the near future, adding a feature will be very fast. As I use Cursor as my IDE, I already see how it’s really easy to go fast with a new feature.

This side-project also underlined how efficient Filament is. It’s really easy to do what you want, once you understand the link between Filament component and Livewire custom components.

Last year, I wrote a blog post, underlining that Filament PHP was not ready for an alternative a full-featured CMS like WordPress. This may be part of a next challenge: tackle this idea. I’m tired of WordPress, so I may try again to make my own custom CMS. I feel we are not very far away with all the available plugins.

Thanks to

Thanks to Filament developers. It’s an extremely well developed product. Also, thanks to the following plugin developers.

  • filament/spatie-laravel-tags-plugin
  • flowframe/laravel-trend
  • guava/calendar
  • mokhosh/filament-kanban
  • solution-forest/filament-tree
  • Spatie

I have been particularly impressed by the quality of Guava Calendar and Mokhosh Filament Kanban.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *