From 2bf9e3d9e099f165b8e74ddcbe736151d9d9364e Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 3 Apr 2026 12:01:07 +0000 Subject: [PATCH] Autosave: 20260403-120107 --- ai/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 404 bytes ai/__pycache__/local_ai_api.cpython-311.pyc | Bin 0 -> 19874 bytes config/__pycache__/settings.cpython-311.pyc | Bin 5552 -> 5816 bytes config/settings.py | 9 +- core/__pycache__/admin.cpython-311.pyc | Bin 212 -> 2340 bytes core/__pycache__/forms.cpython-311.pyc | Bin 0 -> 9020 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 8591 bytes core/__pycache__/tests.cpython-311.pyc | Bin 0 -> 3617 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 789 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 10967 bytes core/admin.py | 47 +- core/forms.py | 126 +++ core/migrations/0001_initial.py | 74 ++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 4743 bytes core/models.py | 111 ++- core/templates/base.html | 20 +- core/templates/core/index.html | 332 +++--- core/templates/core/workspace_detail.html | 236 +++++ core/tests.py | 45 +- core/urls.py | 6 +- core/views.py | 157 ++- static/css/custom.css | 934 ++++++++++++++++- staticfiles/css/custom.css | 941 +++++++++++++++++- 23 files changed, 2844 insertions(+), 194 deletions(-) create mode 100644 ai/__pycache__/__init__.cpython-311.pyc create mode 100644 ai/__pycache__/local_ai_api.cpython-311.pyc create mode 100644 core/__pycache__/forms.cpython-311.pyc create mode 100644 core/__pycache__/tests.cpython-311.pyc create mode 100644 core/forms.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/templates/core/workspace_detail.html diff --git a/ai/__pycache__/__init__.cpython-311.pyc b/ai/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9450fa186aa18cc132580d58f737c5e3d36d0ca GIT binary patch literal 404 zcma)(y-EW?5XbjEl1NCfwp;Ad+*NiW1XK(OHdbD&HGBm10ep{? za+P3ZC!|Z|E~vG`@S7Q!|IENV_4{4q?P2mPUVZ!s#jnLb$@b7EkBFlJ@rcJVQgIQh zq)1d+q^ec4BE*v`G)V8yE58py^n+tD$nu0f(R>i^^yc zX8rYC4%$tJ5N;SDO;3hlgbG4SVH3Z>rU9*hw#N(FdZJyH&y9k-zNxjVb65eZow51S z*xRb4400-RLWCBMkgQzq_Kua|wS*Jf^YU#7tL{apH#v3$#N7tMGxed?w28n)q A4*&oF literal 0 HcmV?d00001 diff --git a/ai/__pycache__/local_ai_api.cpython-311.pyc b/ai/__pycache__/local_ai_api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e906db1fa6c4f29b7a64b3f917acffc876b79a07 GIT binary patch literal 19874 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 ePYFsOPU&~-F8=#&9ufBbJ86a@tU literal 0 HcmV?d00001 diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d79d6a79890a58443ee5f65d8d558823a19e9aed..92a77374f531b4b5d559fccc1be5d9469b0c35d6 100644 GIT binary patch delta 1179 zcmah{OH5Ni6rFkR^+WN!_SLomr7iNY6s#Zo0OcbpXf$zQaN|T4#`}PWAIR(T6Jt^q z#Hb+woP`=#v4Dl~+l|JRTNfgUi>7hm+EtCl7!wokR8T1-PIAwkJ9qA!dr#(R^NXaq zsHz@euF>0k$D)e9mQ3Rt1fa zz^D{OjukeHN zI)Q3S8rui^d6OKYt^By$Ah$s~x8!7Bs&rfyFRANTccZc7M(518E}GzDN)30?Bp+3L z@dMDaRt44!2iN_8LwrGr)5H9&(xCRT1oW{TAor8Rk>`#%TJEupw4FArD~82qojp|L zADpKhI(~sDPr2Ug58^IC^PaSQ*0R%AO?x;YSsEVZmElpW6#uWL#`2-FL{$;$dCGl~ z`m>YnSF$rEPMfkPHnq^&a}|7O@;4-Z$~C0g>p>i6n8;h zbItOL#0Lfaqi&WodP`GV1HjbJEbOn>KZ^p{~@PEp4X zO&q&*{(j4{5`C{k3uO*n8dnQicGmxzrj*smKDvF=ZOiU(?jPU1?A}^%DIsT3fX_m# SuL9l?U%{Wv1d>z{J^TU!SL^=( delta 831 zcmZuuOHWfl6rQ;=Efss)d)rdVqrKINrPiXnpOK0rXw-#q<%ESMr?yu?$n7;66M+Rr z7H-VAaKVZN8cB?%8;r)ptt&}bm@ZuT1G*+^JX4h@ILZ0G^O*CPGn=_rS^b%=HxfJx z7oOXbkM$P1{W@*Z5sehsje0-@!4LYnM#u)ruTx|q0D)z)|DNpB;w%Wdd_q%;#xf}c zC&@Y{ZTv|kA_%51P)rDk5K6d6AiPFJ1UZ7-1Wh6eQPB*|=rJLN*s?}#3azWWNJR|V zL<_V-d{6hp-YMmP7GxIA(y3WgmuC?hWl6IZQ~$mOjz9-iKf2mrQ_%`(=tL_GnK~Y; zW7eicF52L@XosAL3%W$IZ~#u+J4lE)gxX1^A$R}g9>ut=!++1}#g+C#pUg9h z_RFiRgAKr-Twysjgu`w!iwz@JF*+i5*hMx9WAYmtl@GWfr??pvSs359`@DR|)AY3b z$~*K4k%2R!3)p3nnXE)Tt5m-Cj#Iz<;l1K9@F(JOy5ZwM1hovUHkRFMj$6KCyQPfg z=;*BS(p=dstM5)Gogk8x1X8&L9g=EGQRy%Ker~SRES|37{*|%J$Uzvjnh7=4d7*Q~WfGy@{Y}d)l`=MA$ zwRKDdYSW^p(3?y!kWt@AASHkK?_F_K6s^K;>amj&jxMC$D(3A^-pY diff --git a/config/settings.py b/config/settings.py index 291d043..c6b38a4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -23,6 +23,7 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true" ALLOWED_HOSTS = [ "127.0.0.1", "localhost", + "testserver", os.getenv("HOST_FQDN", ""), ] @@ -150,9 +151,11 @@ STATIC_URL = 'static/' STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [ - BASE_DIR / 'static', - BASE_DIR / 'assets', - BASE_DIR / 'node_modules', + path for path in [ + BASE_DIR / 'static', + BASE_DIR / 'assets', + BASE_DIR / 'node_modules', + ] if path.exists() ] # Email diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e8987a0cd478c226e501b5189550e758662c560..6e4b522c8c55071ad05f02251884eac4be125e9e 100644 GIT binary patch literal 2340 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 QqdOGRUK6<80Ex~1A1gb*%>V!Z literal 209 zcmZ3^%ge<81a&?uGA)7hV-N=hn4pZ$LO{lJh7^Vr#vF!R#wbQch7_h?22JLdAO)I? zx7c#?Q&Mw^{WO_wai(M?=B4NBr6d(G10`27do?nz*T#%TYs-K)+l&TLgMz5gq7l#dyU7C|>SHuC-&IrWCvOwYkGb1D84F;JD K*iaE0Pz3-{*EE9w diff --git a/core/__pycache__/tests.cpython-311.pyc b/core/__pycache__/tests.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab6f8222e237e7f1aa5b43e5d54136788b58962b GIT binary patch literal 3617 zcma)8O>Emn79Q%C^1mxNcAB7(W7w{oT9#upZIX?D>b40AuiM5!oEnQI7|q0@LyGFm zD6R(2n4!9y8)AaS4UcMTBEtA{-~ugD^sm;oqPY8Ouv3lXxb&8Of2a5XR?K zMxY(1SE*`zmFNzIddLtBFj5kb$>!0sY~RwRs8LRk6| zt%Pw1cm#)mM{xv}jynB8T_5V^y0C)fJw-y9I00ex^M3%li#E|3yc@LLD(4L%?wuKo zVCezKJoe>+z1z|z+V)SxR{)JoX)WUQap*w^bilts64fAv6A-rg=S>67j;EP8%JjN-mYJ9n%j+5`awoh>I29H+?J8r0@((^QtGd(9xXQ~+ z$(xusU5%M}v&dDg$TA@(!iavuktty%v&aY&Ca2RcI`bwa6I`5ZwzT1-L`xCoC?$23 zD7vX)X1(=)e33m&IsuB=r@-zYJK6u$`MWcJow@(c!!zH^d^=N3PSuiATe2;8Je3D_ z<$-$xwWH$?Syi5@$x{`1%AGj2D<8XesWLKCm1k@6Y{h$?gHjam|7$Sd`u{)RMaTdN zhj3WLIF8{smT>~N;db1CJ8>8H{fy9>-+llJ9t*WD1)`&SrzaQ@7RVeP5x6%fiC_D= zTf|Q@P7aR=-nq9hyl^t8V^a!7%k2>FYD%vG7BpyEoc963(oQO)5Vhg!keLh z9etbOwRTTtRoXccv?JJa<8# zt5)x_N=d;iGm^fc@o}@vt@tOGKN`>FCaw5nE|*Ks%~?H{RC?W{=_O7H;pxeolbACY zClsrO6J0PlCswa6UbEy2)hOvCn>P(BacW9v0D95?2_+?12^OJ@r#~;7kXM>nbRuWp z&E=f-e3@~`4?1-wXLSfjO0%+IsC2{XTFMs)F6$(HOJf?RG1wm<9jx{x#r4l%;T!aM_u5q1MN1Kyy zTAHD9f|XmUUJgv@>IQgJ;RJjF60P=o=bLOQDv~d_0&Xp}YIg?>aJyDqRa8zz6vO#< z22R)M@wF(J06OZAz(p!uWtko-klP6t&2=XsY8xgxR~?ygdP8)n2xuo#GGRdAFO7?S z+`KMa#Ha{tr@H~XmkHLW7o_@sJE2D&AlC2b1wz#S5YB0!+r&J2mL(ywmovzesj_u z9J2?f>^Cy@$hbXv&K^B&pL*ZUOxT$-_TYp)H1*Ti>7BP8Wp}dwI{Rd>a(Tc$0WqGxlDbzBB`iY)oTO9%0iq}t~)WBEgpC*TPlSB6}KK$^TkH7u+@5|NX zg=W&io-N-A4TE!0vATM4&P9N(3X-@A5yy)rTP}mFYry4eHThaazSb-?N54MteYQ3>T^oC^D$m#C`HJ_@Be2mxVG*h87M5AJuyE*l z)GQv`e%bft#Jf?1b|S$mm0J${>%_M3>;d9F?1GjkTr-3fE&&Y3f*`pM+(Q8FA{r0S zW~f;za6G8P1@A-4$7Tr2uPzIg`h{ixS1yz%7U=+3Mvnp0dPX5D!}J8So!$dz;dJlA z2$~jO4hkmfjMMh12goeb?q(nbg$Qm(ZeqAO>ZW2NMKj_)&X}|G~ zeSEMUM}4P%Ly^9=-v!%B)Z5%FY22kMr!M^P*^htvVX-o_R2^EX0Z(43C9k~Ny=q%O z?6$S7)kZE<<>{I{UGW|_Ef(SCDJyOPQWU36Q4CX*IB0h$inw@+lBFoL4|YQ*1bZ9U zBjkSU0v8n_<}z_uQRU(6(&cD$w* zS55a)N`*K5kuud;vCsXDc47qS0gAIW@8=>TydKSKIn*T!nLu1R+bQ&ha)sg)L vSUoIBl8w@p;A5i`mEdEe!AkJ?8J(;IpL%RWf-N3o_X@v>(ccf5a!vdn4m**+ literal 0 HcmV?d00001 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988a223abc7b5af333902925bc196186bbf5..7a38e2e4f222a7b5286b24d789d9ebc7192f0e04 100644 GIT binary patch literal 789 zcma)2&1(}u6rahy{pu!dD4s+K9s~m=gEx`TQ!g=z7xl6S!@4u1Yxg70?zXmwu;LZE}-h01!C#}{RlIEU#^S)z*ewL3b zGndZA7wNn~6jAJ>0NWTN+MtTx2ozfhz=lAz)j+egK(}>_8i-*EOr`2)?Y)A~CwYEb zh3HJDMlE78jjq)q76aO>MOsXu>whCt)i&tHBh+o5myNr)fSHr*p34uSu39u5N+*t+ zc>7**Sgekwpfw;b`96A9z zy0#+3r0<2v{n_3&;pG62uGc|TS;CEf5-LH2YbD`kU4<+CgsYPCuxl0;eeQ(QsF!j- z=H<)_b>CwLaYd*qRhp}&m9)Z=R5_T0@i6{~E@TS2+#==wN%|}ela#QR%uSPsla&PG zMqI+OM{jm`Y4n4Lras%|n=-kaZ~RqW3k73*hHi*I_Yd?${NG+^NHym>$D7BG-)aIn z8FX^!oPjal5oSl+x-0HI5HQSOn8R=xzWwgiX-~i)gFz01Rq(awj|7Y}80RovM(=#s YI(;f&l))&6(ZbR;8gr{$ye-xL0)4vPr~m)} delta 255 zcmbQrcAKewIWI340}#~ttjNp)(vLwL7+{4mKHC5p(-~42QW$d>av7r-85vTTQkZj? za+#x;85x)uQW;ZNQkhd&*RU;PW?)zi#1N3q7{!vp9?YQ0@e(AU$#{#UAh9IlB_ouR zA)23?dW)e5WD!U*FEKaOPm^`>X-2upe;Iva_<-UdwZ&dQ;sY}yBjXJQnG2}s0fYDj pRP=$3ft9ntr9-GAWCq&>7Ws=T@>f{oKQJ@#Gc|C7U=a_{H~_iOH$wmb diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2f0989c7137f7930a63ea76fcd63516c4974e834..3963773590b713c7de4dd810910e046c685f306e 100644 GIT binary patch literal 10967 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 Xr9HlS=yH2}4P;gNa)91vejfh;`i@)& diff --git a/core/admin.py b/core/admin.py index 8c38f3f..baa445e 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,48 @@ from django.contrib import admin -# Register your models here. +from .models import ActivityItem, QuoteLine, SalesWorkspace + + +class QuoteLineInline(admin.TabularInline): + model = QuoteLine + extra = 0 + + +class ActivityItemInline(admin.TabularInline): + model = ActivityItem + extra = 0 + + +@admin.register(SalesWorkspace) +class SalesWorkspaceAdmin(admin.ModelAdmin): + list_display = ( + "customer_name", + "project_number", + "opportunity_title", + "stage", + "estimated_value", + "updated_at", + ) + list_filter = ("stage", "layout_template") + search_fields = ( + "customer_name", + "project_name", + "project_number", + "zipcode", + "address", + "opportunity_title", + ) + inlines = [QuoteLineInline, ActivityItemInline] + + +@admin.register(QuoteLine) +class QuoteLineAdmin(admin.ModelAdmin): + list_display = ("product_name", "workspace", "quantity", "unit_price", "created_at") + search_fields = ("product_name", "workspace__customer_name", "workspace__project_number") + + +@admin.register(ActivityItem) +class ActivityItemAdmin(admin.ModelAdmin): + list_display = ("title", "workspace", "activity_type", "due_at", "owner", "is_done") + list_filter = ("activity_type", "is_done") + search_fields = ("title", "workspace__customer_name", "owner") diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..21e02a9 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,126 @@ +from datetime import timedelta + +from django import forms +from django.utils import timezone + +from .models import ActivityItem, QuoteLine, SalesWorkspace + + +class StyledFormMixin: + def _apply_styles(self): + for name, field in self.fields.items(): + widget = field.widget + css_class = "form-select" if isinstance(widget, forms.Select) else "form-control" + existing = widget.attrs.get("class", "") + widget.attrs["class"] = f"{existing} {css_class} workspace-input".strip() + + +class WorkspaceIntakeForm(StyledFormMixin, forms.ModelForm): + next_meeting_at = forms.DateTimeField( + required=False, + widget=forms.DateTimeInput(attrs={"type": "datetime-local"}), + ) + + class Meta: + model = SalesWorkspace + fields = [ + "customer_name", + "project_name", + "project_number", + "zipcode", + "address", + "city", + "contact_name", + "contact_email", + "contact_phone", + "opportunity_title", + "estimated_value", + "layout_template", + "summary", + "next_step", + "next_meeting_at", + ] + widgets = { + "summary": forms.Textarea(attrs={"rows": 3}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._apply_styles() + placeholders = { + "customer_name": "Van der Meer Construction", + "project_name": "Renovation of showroom", + "project_number": "PRJ-24018", + "zipcode": "3011 AA", + "address": "Binnenweg 18", + "city": "Rotterdam", + "contact_name": "Eva Jansen", + "contact_email": "eva@example.com", + "contact_phone": "+31 6 12345678", + "opportunity_title": "Lighting upgrade quotation", + "estimated_value": "18500", + "summary": "Existing customer requesting quick quote with site visit.", + "next_step": "Confirm site meeting and draft first quotation.", + } + for name, placeholder in placeholders.items(): + self.fields[name].widget.attrs.setdefault("placeholder", placeholder) + self.fields["layout_template"].widget.attrs.setdefault("aria-label", "Workspace template") + + def clean_next_meeting_at(self): + meeting = self.cleaned_data.get("next_meeting_at") + if meeting and meeting < timezone.now(): + raise forms.ValidationError("Choose a future time for the next meeting.") + return meeting + + +class StageUpdateForm(StyledFormMixin, forms.Form): + stage = forms.ChoiceField(choices=SalesWorkspace.Stage.choices) + + def __init__(self, *args, **kwargs): + current_stage = kwargs.pop("current_stage", None) + super().__init__(*args, **kwargs) + self._apply_styles() + if current_stage: + self.fields["stage"].initial = current_stage + + +class QuoteLineForm(StyledFormMixin, forms.ModelForm): + class Meta: + model = QuoteLine + fields = ["product_name", "description", "quantity", "unit_price"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._apply_styles() + self.fields["product_name"].widget.attrs.setdefault("placeholder", "Product or service") + self.fields["description"].widget.attrs.setdefault("placeholder", "Optional scope note") + self.fields["quantity"].widget.attrs.setdefault("min", 1) + self.fields["unit_price"].widget.attrs.setdefault("min", 0) + self.fields["unit_price"].widget.attrs.setdefault("step", "0.01") + + +class ActivityForm(StyledFormMixin, forms.ModelForm): + due_at = forms.DateTimeField( + widget=forms.DateTimeInput(attrs={"type": "datetime-local"}) + ) + + class Meta: + model = ActivityItem + fields = ["title", "activity_type", "due_at", "owner", "notes"] + widgets = { + "notes": forms.Textarea(attrs={"rows": 3}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._apply_styles() + self.fields["title"].widget.attrs.setdefault("placeholder", "Schedule follow-up or task") + self.fields["owner"].widget.attrs.setdefault("placeholder", "Rep or team owner") + self.fields["notes"].widget.attrs.setdefault("placeholder", "Talking points, reminders, dependencies") + self.fields["due_at"].initial = (timezone.now() + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M") + + def clean_due_at(self): + due_at = self.cleaned_data["due_at"] + if due_at < timezone.now() - timedelta(minutes=5): + raise forms.ValidationError("Activity time should be now or in the future.") + return due_at diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..f8bce9f --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# Generated by Django 5.2.7 on 2026-04-03 11:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='SalesWorkspace', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('customer_name', models.CharField(max_length=160)), + ('project_name', models.CharField(blank=True, max_length=160)), + ('project_number', models.CharField(blank=True, max_length=60)), + ('zipcode', models.CharField(blank=True, max_length=20)), + ('address', models.CharField(blank=True, max_length=255)), + ('city', models.CharField(blank=True, max_length=120)), + ('contact_name', models.CharField(blank=True, max_length=160)), + ('contact_email', models.EmailField(blank=True, max_length=254)), + ('contact_phone', models.CharField(blank=True, max_length=40)), + ('opportunity_title', models.CharField(max_length=180)), + ('stage', models.CharField(choices=[('new', 'New lead'), ('qualified', 'Qualified'), ('proposal', 'Proposal / Quote'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost')], default='new', max_length=20)), + ('estimated_value', models.DecimalField(decimal_places=2, default=0, max_digits=12)), + ('layout_template', models.CharField(choices=[('customer360', 'Customer 360'), ('quotation_focus', 'Quotation focus'), ('planning', 'Planning board')], default='customer360', max_length=24)), + ('summary', models.TextField(blank=True)), + ('next_step', models.CharField(blank=True, max_length=180)), + ('next_meeting_at', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['-updated_at', '-created_at'], + }, + ), + migrations.CreateModel( + name='QuoteLine', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product_name', models.CharField(max_length=140)), + ('description', models.CharField(blank=True, max_length=255)), + ('quantity', models.PositiveIntegerField(default=1)), + ('unit_price', models.DecimalField(decimal_places=2, max_digits=10)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quote_lines', to='core.salesworkspace')), + ], + options={ + 'ordering': ['created_at', 'id'], + }, + ), + migrations.CreateModel( + name='ActivityItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=180)), + ('activity_type', models.CharField(choices=[('call', 'Call'), ('meeting', 'Meeting'), ('email', 'Email'), ('task', 'Task')], max_length=20)), + ('due_at', models.DateTimeField()), + ('owner', models.CharField(blank=True, max_length=120)), + ('notes', models.TextField(blank=True)), + ('is_done', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='core.salesworkspace')), + ], + options={ + 'ordering': ['due_at', 'id'], + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..749880cea2e5662a03b62fe5906501ecd358c4ba GIT binary patch literal 4743 zcmbVQO;8(07M{_+#NWtZV}w9LD8%yG2!n$O@!AW*!aw}jhT3FFQnQ1)kq1jN^2`Vf z`%^ybA%{KWwj8NFIN1sZ4twCR2adjNt7;CaIi+e(s@j86@hOMhecdC8A8Aug2K3bZ z`kU9UzxQ5u!&e<00S+GhtAEP>?&7$A(N6tw*Bal?LE|qR;t-$X)_C}K&L&h>3zX1=3bsI9XhL&7`GcEh8GyG>SHTBAp$aD1%qLZFt3|}7&S7}e zDknc=sLlo((+3}&Ujw?$FsI}B%Y!>Vi?O@R66{p=gN4mty}jvev@SGS-Srmw6`DDi zJu$%e`?9QEIu2ntUJBxvONJH*+9n3fJa-Sg^`Su!?^i6$P98}AL zvz8!hTkQ%D8S2OL{}IFV)Quxoi&(x(UjO_kEwqf1AEI=>b(FHswToyG&Xy(1zgRz0 zR;|tzDTnosRqeT6kTTeE$S7?&9?T_NR=ZR*R57Dr$-u~JD`;|F(ssn>c*pYVSd*ok zyo;^&Ev&7pIu;cvkFAbOQ8&~))>yZ)4-6%EIK-rvVh$@=^+P((vlc%~}2a)s)GYN4QNMo|Gfq9GeOT=_fIyLCg#;)DkrbSijz*SCbX zqd6=gQ21XIVd)z(km7$*Z-U+j-WOC|%Iyk|YprN9`lP5D7)IL^oK+2(9pzTr(n*I- zv8^hW^J7(kwkxOVhUHoSsSSfG38&@FJX7Ti)-5l>8&WZ6*lcxS-GGolaKtSsSHu;1 z=4w<_X!gR3od{UpQDh~%+xfWGimt1YhO`f;bC;6G9#x(Qi+1-~>z-M|H_i#78Y8(>0|jD*96xRhcpX9a2rNeB2^jw{V z1zU^Nj&K1h2rC(QTca0bt1ZR$qD6WO*63B$YM+&}Gq6jgY0NBt>Y1e3%>eVO^06?$ zz1;GqW>!)&bN9d-RfRo5vCRe40DhU*Zh-t|c1* zPST(!Pj39lv9YlmVvUi}!j497@=VYpTo>Svh*yoDZ#yr%nyrI@jiA2#zuU_ia&0d`dMmZQIvGM&8Gd6ydA+gy~Y}Sm; z5+PL%M##{O{Y&Q14S2Jg3@w(17R{kWA}oPjGCZ+AX%0^u#ovsR;ib~>k~zFYgynKD zM8enh(q{PD;mzY&5}q%G=gsgu5$>NIN|EsWQuw|ZzE6Y)6RN__EdA<#=KPMyw@iX2C_Br+ z7>VE7Pn+>u$1lj!74qA463>+488eF`a!-^n`8$rp}4Qq?2pBAwyAB{~C0 zm;oecg3B<&DSkxQ zG9U-y%cc0T8Mk>FAd!i^Ei*E4+(RPiQY39gY%&@HN2??gwGo}^&-9a8Wl(^kmPOlfGw9GW4*Y&kecqGS6W zGdgx?ym6A~LMggnMi+>%SPu4+fvLS;n*&qFK{Ajk4W!J06cOgiL2!JWu4w#th&+Bu zA}giHiWylU!YUhUy;>kINy$qdjr=2FPCqoKSIp^8$>uiD{s;ak4NGLj0r3C{@c>EV zvFfOH`GAfdO2=nk-z4ENn~vU2N(YFyR8$8~=!m=n9gRFZ%#hG@DKu?{rit*;sicO< zZ!b_6PRrjoKBcPR�du!B3FX&(9s@U?1sE?oFEg$x{Cfv;PJW#!n^uVGq+23{4W@7R$dRt?%TKn}qL_!gtK@9U|QQ8QgAxYrp58P!A(d8MjXv zw~Xx|Fx(vgZZA`AKiM~6Td}$A7bv$N-cnKhn7@6&^Xeh}(YEVbWWr?5YK57im&+e~x9#KVj`lm~(!*WPfv7rpp09d1eu!{U h`?t(pA+3I^o)ph}s$6UNWxy2T-?72B{|Can`5*OT4Uqr< literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 71a8362..5b23dfb 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,110 @@ -from django.db import models +from decimal import Decimal -# Create your models here. +from django.db import models +from django.utils import timezone + + +class SalesWorkspace(models.Model): + class Stage(models.TextChoices): + NEW = "new", "New lead" + QUALIFIED = "qualified", "Qualified" + PROPOSAL = "proposal", "Proposal / Quote" + NEGOTIATION = "negotiation", "Negotiation" + WON = "won", "Won" + LOST = "lost", "Lost" + + class LayoutTemplate(models.TextChoices): + CUSTOMER_360 = "customer360", "Customer 360" + QUOTATION_FOCUS = "quotation_focus", "Quotation focus" + PLANNING = "planning", "Planning board" + + customer_name = models.CharField(max_length=160) + project_name = models.CharField(max_length=160, blank=True) + project_number = models.CharField(max_length=60, blank=True) + zipcode = models.CharField(max_length=20, blank=True) + address = models.CharField(max_length=255, blank=True) + city = models.CharField(max_length=120, blank=True) + contact_name = models.CharField(max_length=160, blank=True) + contact_email = models.EmailField(blank=True) + contact_phone = models.CharField(max_length=40, blank=True) + opportunity_title = models.CharField(max_length=180) + stage = models.CharField(max_length=20, choices=Stage.choices, default=Stage.NEW) + estimated_value = models.DecimalField(max_digits=12, decimal_places=2, default=0) + layout_template = models.CharField( + max_length=24, choices=LayoutTemplate.choices, default=LayoutTemplate.CUSTOMER_360 + ) + summary = models.TextField(blank=True) + next_step = models.CharField(max_length=180, blank=True) + next_meeting_at = models.DateTimeField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["-updated_at", "-created_at"] + + def __str__(self): + project = f" · {self.project_number}" if self.project_number else "" + return f"{self.customer_name} — {self.opportunity_title}{project}" + + @property + def quote_total(self): + total = sum((line.subtotal for line in self.quote_lines.all()), Decimal("0.00")) + return total.quantize(Decimal("0.01")) if total else Decimal("0.00") + + @property + def open_activities_count(self): + return self.activities.filter(is_done=False).count() + + @property + def pipeline_label(self): + return self.get_stage_display() + + @property + def next_meeting_is_upcoming(self): + return bool(self.next_meeting_at and self.next_meeting_at >= timezone.now()) + + +class QuoteLine(models.Model): + workspace = models.ForeignKey( + SalesWorkspace, related_name="quote_lines", on_delete=models.CASCADE + ) + product_name = models.CharField(max_length=140) + description = models.CharField(max_length=255, blank=True) + quantity = models.PositiveIntegerField(default=1) + unit_price = models.DecimalField(max_digits=10, decimal_places=2) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["created_at", "id"] + + def __str__(self): + return f"{self.product_name} × {self.quantity}" + + @property + def subtotal(self): + return (self.unit_price or Decimal("0.00")) * self.quantity + + +class ActivityItem(models.Model): + class ActivityType(models.TextChoices): + CALL = "call", "Call" + MEETING = "meeting", "Meeting" + EMAIL = "email", "Email" + TASK = "task", "Task" + + workspace = models.ForeignKey( + SalesWorkspace, related_name="activities", on_delete=models.CASCADE + ) + title = models.CharField(max_length=180) + activity_type = models.CharField(max_length=20, choices=ActivityType.choices) + due_at = models.DateTimeField() + owner = models.CharField(max_length=120, blank=True) + notes = models.TextField(blank=True) + is_done = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["due_at", "id"] + + def __str__(self): + return f"{self.title} ({self.get_activity_type_display()})" diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..0ca5cdf 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,25 @@ +{% load static %} - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} + + {% block title %}{{ meta_title|default:project_name|default:"FlowDesk Sales" }}{% endblock %} + {% if project_image_url %} {% endif %} - {% load static %} + + + + + {% block head %}{% endblock %} - {% block content %}{% endblock %} + - diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..53528cc 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,195 @@ {% extends "base.html" %} +{% load static %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}{{ meta_title }}{% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… -
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-{% endblock %} \ No newline at end of file +
+ + +
+
+
+ Web-first sales cockpit +

Compact customer workspace built for lead → quote → won.

+

Default concept for your Odoo-connected UI: searchable customer/project context, persistent icon-only navigation, and dockable panels for Opportunities, Quotations, Projects, Agenda, and Tasks.

+ +
+ +
+ +
+ + Admin +
+ + {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + +
+
+ Tracked workspaces + {{ workspace_count }} +

Customer or project contexts accessible from one search bar.

+
+
+ Active deals + {{ active_count }} +

Fast switching without leaving the main canvas.

+
+
+ Won + {{ won_count }} +

Deals ready to hand over into project delivery later.

+
+
+ Pipeline value + €{{ pipeline_total|floatformat:0 }} +

Compact overview for managers and sales reps.

+
+
+ +
+
+
+
+ Saved templates +

Pick the right focus instantly

+
+
+
+ {% for value, label in layout_choices %} +
+
+
+

{{ label }}

+

{% if value == 'customer360' %}See opportunity, agenda, projects, and quotation side by side.{% elif value == 'quotation_focus' %}Give most space to the quote editor when speed matters.{% else %}Keep projects and follow-ups visible during planning calls.{% endif %}

+
+
+ {% endfor %} +
+
+ +
+
+
+ First workflow widget +

Create a sales workspace

+
+ Lead intake → detail workspace +
+
+ {% csrf_token %} +
+ {% for field in create_form %} +
+ + {{ field }} + {% if field.errors %}
{{ field.errors|join:', ' }}
{% endif %} +
+ {% endfor %} +
+
+ +

This creates the customer/project context and opens the single-screen detail view.

+
+
+
+
+ +
+
+
+
+ Pipeline list +

Recent workspaces

+
+
+ {% if workspaces %} + + {% else %} +
+ +

No workspaces yet

+

Create the first customer/project context above to preview the one-screen layout.

+
+ {% endif %} +
+ +
+
+
+ Agenda snapshot +

Upcoming tasks

+
+
+ {% if upcoming_items %} +
+ {% for item in upcoming_items %} +
+
+
+ {{ item.title }} +

{{ item.workspace.customer_name }} · {{ item.get_activity_type_display }} · {{ item.due_at|date:"d M H:i" }}

+
+
+ {% endfor %} +
+ {% else %} +
+ +

Upcoming meetings and actions appear here after you create a workspace.

+
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/core/templates/core/workspace_detail.html b/core/templates/core/workspace_detail.html new file mode 100644 index 0000000..713fe9f --- /dev/null +++ b/core/templates/core/workspace_detail.html @@ -0,0 +1,236 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}{{ meta_title }}{% endblock %} + +{% block content %} +
+ + +
+
+
+ Back to pipeline +

{{ workspace.customer_name }}

+

{{ workspace.opportunity_title }}{% if workspace.project_number %} · {{ workspace.project_number }}{% elif workspace.project_name %} · {{ workspace.project_name }}{% endif %}

+
+
+ {{ workspace.get_stage_display }} +
€{{ workspace.estimated_value|floatformat:0 }}
+
+
+ + {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + +
+
+
+ + +
+
+ Admin +
+ +
+
+
+
+ Opportunity +

Lead status

+
+ Single-screen control +
+
+
Customer{{ workspace.customer_name }}
+
Contact{{ workspace.contact_name|default:"Not added yet" }}
+
Email{{ workspace.contact_email|default:"Not added yet" }}
+
Phone{{ workspace.contact_phone|default:"Not added yet" }}
+
Next step{{ workspace.next_step|default:"Define the next best action" }}
+
+
+ {% csrf_token %} +
+ + {{ stage_form.stage }} +
+ +
+
{{ workspace.summary|default:"Add qualification notes, buying signals, and constraints here." }}
+
+ +
+
+
+ Projects +

Project context

+
+ +
+
+
Project{{ workspace.project_name|default:"Current customer record" }}
+
Project #{{ workspace.project_number|default:"To be assigned" }}
+
Zip code{{ workspace.zipcode|default:"—" }}
+
Address{{ workspace.address|default:"No address yet" }}
+
City{{ workspace.city|default:"—" }}
+
+
+ +
+
+
+ Quotations +

Quotation focus

+
+ Takes the most space when needed +
+
+ {% csrf_token %} +
+ {% for field in quote_form %} +
+ + {{ field }} + {% if field.errors %}
{{ field.errors|join:', ' }}
{% endif %} +
+ {% endfor %} +
+
+ +

Adding a quote line moves early leads into Proposal automatically.

+
+
+ + {% if quote_lines %} +
+ + + + + + + + + + + + {% for line in quote_lines %} + + + + + + + + {% endfor %} + +
ItemDescriptionQtyUnitSubtotal
{{ line.product_name }}{{ line.description|default:"—" }}{{ line.quantity }}€{{ line.unit_price|floatformat:2 }}€{{ line.subtotal|floatformat:2 }}
+
+
Quote total €{{ workspace.quote_total|floatformat:2 }}
+ {% else %} +
+ +

No quotation lines yet. Start with one service or product to preview the dense editor.

+
+ {% endif %} +
+ +
+
+
+ Agenda +

Meeting planning

+
+
+
+ {% if workspace.next_meeting_at %} +
+
+
+ Next meeting +

{{ workspace.next_meeting_at|date:"D d M · H:i" }}{% if workspace.next_meeting_is_upcoming %} · upcoming{% endif %}

+
+
+ {% endif %} + {% if upcoming_activities %} + {% for item in upcoming_activities|slice:":4" %} +
+
+
+ {{ item.title }} +

{{ item.get_activity_type_display }} · {{ item.due_at|date:"d M H:i" }}

+
+
+ {% endfor %} + {% else %} +
+ +

Add a call or meeting to keep the deal moving.

+
+ {% endif %} +
+
+ +
+
+
+ Activities / Tasks +

Schedule the next action

+
+
+
+ {% csrf_token %} +
+ {% for field in activity_form %} +
+ + {{ field }} + {% if field.errors %}
{{ field.errors|join:', ' }}
{% endif %} +
+ {% endfor %} +
+
+ +
+
+ + {% if activities %} +
+ {% for item in activities %} +
+
+ {{ item.title }} +

{{ item.get_activity_type_display }} · {{ item.due_at|date:"d M H:i" }}{% if item.owner %} · {{ item.owner }}{% endif %}

+
+ {% if item.is_done %}Done{% else %}Open{% endif %} +
+ {% endfor %} +
+ {% else %} +
+ +

No activities yet. Add one to make the workspace actionable.

+
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/core/tests.py b/core/tests.py index 7ce503c..eefa89e 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,46 @@ from django.test import TestCase +from django.urls import reverse +from django.utils import timezone -# Create your tests here. +from .models import SalesWorkspace + + +class SalesWorkspaceFlowTests(TestCase): + def test_home_page_loads(self): + response = self.client.get(reverse("home")) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Compact sales workspace") + + def test_create_workspace_redirects_to_detail(self): + payload = { + "workspace-customer_name": "Northwind BV", + "workspace-project_name": "Warehouse fit-out", + "workspace-project_number": "PRJ-001", + "workspace-zipcode": "1000 AA", + "workspace-address": "Harbor Street 10", + "workspace-city": "Amsterdam", + "workspace-contact_name": "Lotte", + "workspace-contact_email": "lotte@example.com", + "workspace-contact_phone": "+31000000", + "workspace-opportunity_title": "Prepare first quotation", + "workspace-estimated_value": "9800", + "workspace-layout_template": "customer360", + "workspace-summary": "Test summary", + "workspace-next_step": "Schedule visit", + "workspace-next_meeting_at": (timezone.now() + timedelta(days=1)).strftime("%Y-%m-%dT%H:%M"), + } + response = self.client.post(reverse("home"), payload) + workspace = SalesWorkspace.objects.get(customer_name="Northwind BV") + self.assertRedirects(response, reverse("workspace_detail", args=[workspace.pk])) + self.assertEqual(workspace.stage, SalesWorkspace.Stage.NEW) + + def test_workspace_detail_shows_panels(self): + workspace = SalesWorkspace.objects.create( + customer_name="Northwind BV", + opportunity_title="Prepare first quotation", + estimated_value=10000, + ) + response = self.client.get(reverse("workspace_detail", args=[workspace.pk])) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Quotations") + self.assertContains(response, workspace.customer_name) diff --git a/core/urls.py b/core/urls.py index 6299e3d..2a3cfc5 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,11 @@ from django.urls import path -from .views import home +from .views import add_activity, add_quote_line, home, update_stage, workspace_detail urlpatterns = [ path("", home, name="home"), + path("workspaces//", workspace_detail, name="workspace_detail"), + path("workspaces//stage/", update_stage, name="update_stage"), + path("workspaces//quote/", add_quote_line, name="add_quote_line"), + path("workspaces//activity/", add_activity, name="add_activity"), ] diff --git a/core/views.py b/core/views.py index c9aed12..b291827 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,148 @@ -import os -import platform - -from django import get_version as django_version -from django.shortcuts import render +from django.contrib import messages +from django.db.models import Q +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone +from .forms import ActivityForm, QuoteLineForm, StageUpdateForm, WorkspaceIntakeForm +from .models import SalesWorkspace + + +def _base_shell_context(): + return { + "project_name": "FlowDesk Sales", + "project_tagline": "Compact sales workspace for fast lead-to-quote execution", + "meta_description": ( + "Single-screen sales workspace with compact navigation, opportunity tracking, " + "quotation drafting, projects, and agenda panels." + ), + "nav_items": [ + {"icon": "bi-grid-1x2-fill", "label": "Workspace", "url": "/"}, + {"icon": "bi-people-fill", "label": "Contacts", "url": "/"}, + {"icon": "bi-graph-up-arrow", "label": "Opportunities", "url": "/"}, + {"icon": "bi-receipt-cutoff", "label": "Quotations", "url": "/"}, + {"icon": "bi-kanban-fill", "label": "Projects", "url": "/"}, + {"icon": "bi-calendar3", "label": "Agenda", "url": "/"}, + {"icon": "bi-shield-lock-fill", "label": "Admin", "url": "/admin/"}, + ], + } + + +def _workspace_queryset(query=None): + queryset = SalesWorkspace.objects.prefetch_related("quote_lines", "activities") + if query: + queryset = queryset.filter( + Q(customer_name__icontains=query) + | Q(project_name__icontains=query) + | Q(project_number__icontains=query) + | Q(zipcode__icontains=query) + | Q(address__icontains=query) + | Q(opportunity_title__icontains=query) + ) + return queryset + def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + query = request.GET.get("q", "").strip() + if request.method == "POST": + create_form = WorkspaceIntakeForm(request.POST, prefix="workspace") + if create_form.is_valid(): + workspace = create_form.save(commit=False) + workspace.stage = SalesWorkspace.Stage.NEW + workspace.save() + if workspace.next_meeting_at: + workspace.activities.create( + title="Initial meeting", + activity_type="meeting", + due_at=workspace.next_meeting_at, + owner=workspace.contact_name or "Sales", + notes="Auto-created from workspace intake.", + ) + messages.success(request, f"{workspace.customer_name} workspace created.") + return redirect("workspace_detail", pk=workspace.pk) + messages.error(request, "Please review the highlighted fields and try again.") + else: + create_form = WorkspaceIntakeForm(prefix="workspace") + + workspaces = list(_workspace_queryset(query)[:8]) + upcoming_items = [] + for workspace in workspaces: + upcoming_items.extend([item for item in workspace.activities.all() if not item.is_done]) + upcoming_items.sort(key=lambda item: item.due_at) + + all_workspaces = _workspace_queryset() + active_workspaces = [item for item in all_workspaces if item.stage not in {SalesWorkspace.Stage.WON, SalesWorkspace.Stage.LOST}] context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + **_base_shell_context(), + "meta_title": "FlowDesk Sales Workspace", + "search_query": query, + "create_form": create_form, + "workspaces": workspaces, + "workspace_count": all_workspaces.count(), + "active_count": len(active_workspaces), + "won_count": sum(1 for item in all_workspaces if item.stage == SalesWorkspace.Stage.WON), + "pipeline_total": sum((item.estimated_value for item in active_workspaces), 0), + "upcoming_items": upcoming_items[:5], + "stage_choices": SalesWorkspace.Stage.choices, + "layout_choices": SalesWorkspace.LayoutTemplate.choices, } return render(request, "core/index.html", context) + + +def workspace_detail(request, pk): + workspace = get_object_or_404(_workspace_queryset(), pk=pk) + context = { + **_base_shell_context(), + "meta_title": f"{workspace.customer_name} · Workspace", + "meta_description": f"Compact sales workspace for {workspace.customer_name} and project {workspace.project_name or workspace.project_number or workspace.opportunity_title}.", + "workspace": workspace, + "stage_form": StageUpdateForm(prefix="stage", current_stage=workspace.stage), + "quote_form": QuoteLineForm(prefix="quote"), + "activity_form": ActivityForm(prefix="activity"), + "quote_lines": workspace.quote_lines.all(), + "activities": workspace.activities.all(), + "upcoming_activities": [item for item in workspace.activities.all() if not item.is_done], + "completed_activities": [item for item in workspace.activities.all() if item.is_done], + } + return render(request, "core/workspace_detail.html", context) + + +def update_stage(request, pk): + workspace = get_object_or_404(SalesWorkspace, pk=pk) + form = StageUpdateForm(request.POST, prefix="stage", current_stage=workspace.stage) + if request.method == "POST" and form.is_valid(): + workspace.stage = form.cleaned_data["stage"] + workspace.save(update_fields=["stage", "updated_at"]) + messages.success(request, f"Stage updated to {workspace.get_stage_display()}.") + else: + messages.error(request, "Could not update the stage. Please choose a valid value.") + return redirect("workspace_detail", pk=workspace.pk) + + +def add_quote_line(request, pk): + workspace = get_object_or_404(SalesWorkspace, pk=pk) + form = QuoteLineForm(request.POST, prefix="quote") + if request.method == "POST" and form.is_valid(): + quote_line = form.save(commit=False) + quote_line.workspace = workspace + quote_line.save() + if workspace.stage in {SalesWorkspace.Stage.NEW, SalesWorkspace.Stage.QUALIFIED}: + workspace.stage = SalesWorkspace.Stage.PROPOSAL + workspace.save(update_fields=["stage", "updated_at"]) + messages.success(request, f"Added quote line: {quote_line.product_name}.") + else: + messages.error(request, "Please correct the quote line fields and try again.") + return redirect("workspace_detail", pk=workspace.pk) + + +def add_activity(request, pk): + workspace = get_object_or_404(SalesWorkspace, pk=pk) + form = ActivityForm(request.POST, prefix="activity") + if request.method == "POST" and form.is_valid(): + activity = form.save(commit=False) + activity.workspace = workspace + activity.save() + messages.success(request, f"Activity scheduled: {activity.title}.") + else: + messages.error(request, "Please correct the activity fields and try again.") + return redirect("workspace_detail", pk=workspace.pk) diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..fc38fd5 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,932 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* FlowDesk Sales custom UI */ +:root { + --fd-bg: #edf4f6; + --fd-bg-deep: #0f172a; + --fd-surface: rgba(255, 255, 255, 0.84); + --fd-surface-strong: #ffffff; + --fd-text: #102133; + --fd-muted: #5f7187; + --fd-border: rgba(15, 23, 42, 0.08); + --fd-primary: #0f766e; + --fd-secondary: #14b8a6; + --fd-accent: #f97316; + --fd-accent-soft: rgba(249, 115, 22, 0.16); + --fd-warning: #f59e0b; + --fd-success: #10b981; + --fd-danger: #ef4444; + --fd-shadow: 0 30px 60px rgba(15, 23, 42, 0.12); + --fd-radius-xl: 28px; + --fd-radius-lg: 22px; + --fd-radius-md: 16px; + --fd-radius-sm: 12px; +} + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + margin: 0; + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + color: var(--fd-text); + background: + radial-gradient(circle at top left, rgba(20, 184, 166, 0.22), transparent 24%), + radial-gradient(circle at top right, rgba(249, 115, 22, 0.12), transparent 20%), + linear-gradient(160deg, #f8fbfc 0%, #edf4f6 45%, #e8eef4 100%); + overflow: hidden; +} + +a { + color: inherit; + text-decoration: none; +} + +h1, +h2, +h3, +h4, +.brand-mark__core { + font-family: 'Manrope', 'Inter', sans-serif; + letter-spacing: -0.03em; +} + +.app-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 88px minmax(0, 1fr); +} + +.app-sidebar { + position: relative; + padding: 24px 18px; + background: rgba(10, 15, 25, 0.95); + display: flex; + flex-direction: column; + align-items: center; + gap: 28px; + border-right: 1px solid rgba(255, 255, 255, 0.06); +} + +.brand-mark { + width: 52px; + height: 52px; + border-radius: 18px; + display: grid; + place-items: center; + color: white; + background: linear-gradient(135deg, var(--fd-secondary), var(--fd-primary)); + box-shadow: 0 16px 30px rgba(20, 184, 166, 0.3); +} + +.brand-mark__core { + font-size: 1.35rem; + font-weight: 800; +} + +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + align-items: center; +} + +.sidebar-link { + width: 52px; + height: 52px; + border-radius: 16px; + display: grid; + place-items: center; + color: rgba(255, 255, 255, 0.76); + font-size: 1.1rem; + position: relative; + transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease; +} + +.sidebar-link:hover, +.sidebar-link:focus-visible, +.sidebar-link.is-active { + color: #fff; + background: rgba(255, 255, 255, 0.1); + transform: translateY(-1px); + outline: none; +} + +.sidebar-link::after { + content: attr(data-label); + position: absolute; + left: 62px; + padding: 8px 12px; + border-radius: 999px; + background: rgba(10, 15, 25, 0.94); + color: #fff; + font-size: 0.83rem; + line-height: 1; + opacity: 0; + transform: translateX(-6px); + pointer-events: none; + transition: 0.2s ease; + white-space: nowrap; +} + +.sidebar-link:hover::after, +.sidebar-link:focus-visible::after { + opacity: 1; + transform: translateX(0); +} + +.app-main { + padding: 24px 28px 28px; + overflow: auto; + display: grid; + gap: 20px; + align-content: start; +} + +.hero-panel, +.surface-card, +.search-card, +.stat-card, +.detail-topbar { + border: 1px solid var(--fd-border); + box-shadow: var(--fd-shadow); +} + +.hero-panel { + position: relative; + overflow: hidden; + display: grid; + grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.9fr); + gap: 16px; + padding: 32px; + border-radius: var(--fd-radius-xl); + background: linear-gradient(135deg, rgba(15, 118, 110, 0.96), rgba(10, 15, 25, 0.98)); + color: white; +} + +.hero-panel::before { + content: ''; + position: absolute; + inset: auto auto -60px -60px; + width: 220px; + height: 220px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.22), transparent 64%); +} + +.hero-copy { + position: relative; + z-index: 1; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.78rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--fd-secondary); + font-weight: 700; +} + +.hero-panel .eyebrow { + color: rgba(255, 255, 255, 0.72); +} + +.hero-copy h1, +.detail-topbar h1 { + margin: 10px 0 12px; + font-size: clamp(2rem, 3vw, 3.5rem); + line-height: 1.02; +} + +.hero-copy h1 span { + color: #a7f3d0; +} + +.hero-copy p, +.detail-topbar p, +.stat-card p, +.template-tile p, +.empty-state p, +.form-actions p, +.workspace-row__meta, +.agenda-item p, +.activity-row p { + color: rgba(255, 255, 255, 0.8); +} + +.hero-copy p { + max-width: 720px; + font-size: 1rem; + margin-bottom: 0; +} + +.hero-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 24px; +} + +.hero-visual { + position: relative; + min-height: 220px; +} + +.shape { + position: absolute; + border-radius: 28px; + backdrop-filter: blur(18px); +} + +.shape-sphere { + width: 142px; + height: 142px; + top: 6px; + right: 40px; + border-radius: 50%; + background: radial-gradient(circle at 32% 32%, rgba(255, 255, 255, 0.95), rgba(20, 184, 166, 0.18) 45%, rgba(255, 255, 255, 0.08) 72%); + box-shadow: inset -20px -20px 42px rgba(255, 255, 255, 0.06); +} + +.shape-card { + width: 240px; + height: 170px; + right: 20px; + bottom: 14px; + padding: 18px; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.18); +} + +.mini-bar { + height: 14px; + width: 110px; + border-radius: 999px; + margin-bottom: 20px; + background: linear-gradient(90deg, rgba(255,255,255,0.9), rgba(255,255,255,0.35)); +} + +.mini-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.mini-grid span { + display: block; + height: 46px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.14); +} + +.shape-cylinder { + width: 84px; + height: 150px; + left: 20px; + bottom: 4px; + border-radius: 999px; + background: linear-gradient(180deg, rgba(249, 115, 22, 0.95), rgba(249, 115, 22, 0.16)); +} + +.top-toolbar, +.detail-toolbar { + display: flex; + gap: 14px; + align-items: center; + justify-content: space-between; +} + +.search-card { + display: flex; + gap: 14px; + align-items: center; + border-radius: var(--fd-radius-lg); + background: rgba(255, 255, 255, 0.72); + padding: 12px; + flex: 1; +} + +.search-card--static { + padding: 10px 12px; +} + +.search-field-wrap { + flex: 1; + display: flex; + align-items: center; + gap: 12px; + padding: 0 10px; + border-radius: 14px; + background: rgba(15, 23, 42, 0.04); +} + +.search-field-wrap i { + color: var(--fd-muted); +} + +.search-field { + width: 100%; + border: 0; + padding: 12px 0; + background: transparent; + color: var(--fd-text); + font-size: 0.98rem; +} + +.search-field:focus { + outline: none; +} + +.admin-pill, +.section-chip, +.stage-pill { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 999px; + padding: 10px 14px; + font-size: 0.82rem; + font-weight: 700; +} + +.admin-pill { + background: rgba(15, 23, 42, 0.94); + color: #fff; +} + +.stats-grid, +.content-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 18px; +} + +.stat-card { + grid-column: span 3; + padding: 22px; + border-radius: 24px; + background: rgba(255, 255, 255, 0.72); +} + +.stat-card strong { + display: block; + margin: 10px 0 8px; + font-size: 2rem; + font-family: 'Manrope', sans-serif; +} + +.stat-label { + color: var(--fd-muted); + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.stat-card p, +.template-tile p, +.empty-state p, +.form-actions p, +.workspace-row__meta, +.agenda-item p, +.activity-row p, +.detail-topbar p, +.info-row span, +.summary-box { + color: var(--fd-muted); +} + +.stat-card--accent { + background: linear-gradient(135deg, rgba(249, 115, 22, 0.96), rgba(255, 149, 60, 0.94)); + color: white; +} + +.stat-card--accent .stat-label, +.stat-card--accent p { + color: rgba(255, 255, 255, 0.85); +} + +.surface-card { + padding: 24px; + border-radius: var(--fd-radius-xl); + background: var(--fd-surface); + backdrop-filter: blur(22px); +} + +.template-card-list { + grid-column: span 4; +} + +.create-card { + grid-column: span 8; +} + +.list-card { + grid-column: span 7; +} + +.agenda-card { + grid-column: span 5; +} + +.section-heading, +.panel-head { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; + margin-bottom: 18px; +} + +.section-heading h2, +.panel-head h2 { + margin: 6px 0 0; + font-size: 1.5rem; +} + +.section-chip { + background: rgba(20, 184, 166, 0.12); + color: var(--fd-primary); +} + +.template-list, +.agenda-list, +.activity-feed { + display: grid; + gap: 12px; +} + +.template-tile, +.agenda-item, +.activity-row, +.workspace-row { + display: flex; + gap: 14px; + align-items: center; + justify-content: space-between; + border-radius: 18px; + padding: 16px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(15, 23, 42, 0.06); +} + +.template-icon, +.agenda-icon { + width: 44px; + height: 44px; + border-radius: 14px; + display: grid; + place-items: center; + background: rgba(20, 184, 166, 0.12); + color: var(--fd-primary); + flex-shrink: 0; +} + +.workspace-form, +.compact-form { + display: grid; + gap: 18px; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 14px; +} + +.form-grid--quote .form-field { + grid-column: span 3; +} + +.form-field { + grid-column: span 4; + display: grid; + gap: 8px; +} + +.form-field--wide { + grid-column: span 6; +} + +.form-field--full { + grid-column: 1 / -1; +} + +.form-field label, +.compact-form__field label { + font-size: 0.84rem; + font-weight: 700; + color: var(--fd-text); +} + +.workspace-input { + border: 1px solid rgba(15, 23, 42, 0.1); + border-radius: 14px; + padding: 0.76rem 0.95rem; + background: rgba(255, 255, 255, 0.96); + color: var(--fd-text); +} + +.workspace-input:focus { + border-color: rgba(20, 184, 166, 0.8); + box-shadow: 0 0 0 0.24rem rgba(20, 184, 166, 0.14); +} + +.field-error { + color: var(--fd-danger); + font-size: 0.82rem; +} + +.form-actions { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: center; + flex-wrap: wrap; +} + +.form-actions--inline { + justify-content: flex-start; +} + +.btn { + border-radius: 14px; + border: none; + padding: 0.85rem 1.2rem; + font-weight: 700; + transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.btn:hover, +.btn:focus-visible { + transform: translateY(-1px); +} + +.btn-accent { + color: white; + background: linear-gradient(135deg, var(--fd-accent), #fb923c); + box-shadow: 0 16px 24px rgba(249, 115, 22, 0.24); +} + +.btn-ghost { + color: white; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.16); +} + +.btn-dark-shell { + color: white; + background: linear-gradient(135deg, #172033, #0f172a); + box-shadow: 0 10px 18px rgba(15, 23, 42, 0.18); +} + +.workspace-list { + display: grid; + gap: 12px; +} + +.workspace-row__title { + font-weight: 800; + font-size: 1.02rem; +} + +.workspace-row__side { + display: grid; + justify-items: end; + gap: 8px; + flex-shrink: 0; +} + +.empty-state { + min-height: 220px; + display: grid; + place-items: center; + text-align: center; + gap: 8px; + border-radius: 22px; + background: rgba(255, 255, 255, 0.58); + border: 1px dashed rgba(15, 23, 42, 0.14); + padding: 20px; +} + +.empty-state i { + font-size: 1.7rem; + color: var(--fd-primary); +} + +.empty-state--small { + min-height: 120px; +} + +.message-stack { + display: grid; + gap: 10px; +} + +.custom-alert { + margin: 0; + border-radius: 16px; + border: 0; + padding: 14px 16px; +} + +.alert-success { + background: rgba(16, 185, 129, 0.14); + color: #0d7a55; +} + +.alert-error { + background: rgba(239, 68, 68, 0.12); + color: #b91c1c; +} + +.detail-topbar { + display: flex; + justify-content: space-between; + gap: 18px; + align-items: end; + padding: 24px 28px; + border-radius: var(--fd-radius-xl); + background: rgba(255, 255, 255, 0.72); +} + +.back-link { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--fd-primary); + font-weight: 700; +} + +.detail-meta { + display: flex; + flex-direction: column; + gap: 10px; + align-items: flex-end; +} + +.detail-value { + font-family: 'Manrope', sans-serif; + font-size: 2.1rem; + font-weight: 800; +} + +.workspace-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 18px; +} + +.panel { + min-height: 260px; + overflow: hidden; +} + +.panel-opportunity { + grid-column: span 4; +} + +.panel-projects { + grid-column: span 3; +} + +.panel-quote { + grid-column: span 5; +} + +.panel-agenda { + grid-column: span 4; +} + +.panel-activities { + grid-column: span 8; +} + +.template-quotation_focus .panel-quote { + grid-column: span 8; +} + +.template-quotation_focus .panel-projects { + grid-column: span 4; +} + +.template-planning .panel-agenda { + grid-column: span 5; +} + +.template-planning .panel-activities { + grid-column: span 7; +} + +.info-stack { + display: grid; + gap: 10px; +} + +.info-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 12px 14px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.72); +} + +.info-row strong { + text-align: right; +} + +.summary-box { + border-radius: 18px; + background: rgba(15, 118, 110, 0.08); + padding: 16px; +} + +.compact-form--inline { + grid-template-columns: 1fr auto; + align-items: end; +} + +.compact-form__field { + display: grid; + gap: 8px; +} + +.panel-action { + border: 0; + background: rgba(15, 23, 42, 0.04); + border-radius: 999px; + padding: 8px 12px; + color: var(--fd-muted); + font-weight: 700; +} + +.quote-table-wrap { + overflow: auto; + border-radius: 18px; + border: 1px solid rgba(15, 23, 42, 0.08); +} + +.quote-table { + margin: 0; +} + +.quote-table thead th { + background: rgba(15, 23, 42, 0.03); + color: var(--fd-muted); + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.08em; + border-bottom: 0; +} + +.quote-table tbody td { + vertical-align: middle; + color: var(--fd-text); + border-color: rgba(15, 23, 42, 0.06); +} + +.quote-total { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 4px 0; + font-weight: 700; +} + +.activity-row { + align-items: flex-start; +} + +.stage-pill { + padding: 8px 12px; +} + +.stage-new { + background: rgba(20, 184, 166, 0.12); + color: var(--fd-primary); +} + +.stage-qualified { + background: rgba(14, 165, 233, 0.14); + color: #0369a1; +} + +.stage-proposal { + background: rgba(245, 158, 11, 0.18); + color: #b45309; +} + +.stage-negotiation { + background: rgba(249, 115, 22, 0.18); + color: #c2410c; +} + +.stage-won { + background: rgba(16, 185, 129, 0.14); + color: #047857; +} + +.stage-lost { + background: rgba(239, 68, 68, 0.12); + color: #b91c1c; +} + +@media (max-width: 1199px) { + body { + overflow: auto; + } + + .stats-grid .stat-card, + .template-card-list, + .create-card, + .list-card, + .agenda-card, + .panel-opportunity, + .panel-projects, + .panel-quote, + .panel-agenda, + .panel-activities { + grid-column: span 12; + } +} + +@media (max-width: 991px) { + .app-shell { + grid-template-columns: 1fr; + } + + .app-sidebar { + position: sticky; + top: 0; + z-index: 20; + flex-direction: row; + justify-content: space-between; + padding: 16px 20px; + } + + .sidebar-nav { + flex-direction: row; + justify-content: center; + overflow: auto; + } + + .sidebar-link::after { + display: none; + } + + .hero-panel, + .detail-topbar { + grid-template-columns: 1fr; + } + + .top-toolbar, + .detail-toolbar, + .search-card, + .form-actions, + .compact-form--inline, + .detail-topbar { + flex-direction: column; + align-items: stretch; + } + + .form-field, + .form-field--wide, + .form-field--full, + .form-grid--quote .form-field { + grid-column: span 12; + } + + .app-main { + padding: 18px; + } +} + +@media (max-width: 575px) { + .hero-panel, + .surface-card, + .detail-topbar { + padding: 20px; + border-radius: 22px; + } + + .hero-copy h1, + .detail-topbar h1 { + font-size: 1.85rem; + } + + .sidebar-link { + width: 46px; + height: 46px; + } } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..fc38fd5 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,932 @@ - +/* FlowDesk Sales custom UI */ :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --fd-bg: #edf4f6; + --fd-bg-deep: #0f172a; + --fd-surface: rgba(255, 255, 255, 0.84); + --fd-surface-strong: #ffffff; + --fd-text: #102133; + --fd-muted: #5f7187; + --fd-border: rgba(15, 23, 42, 0.08); + --fd-primary: #0f766e; + --fd-secondary: #14b8a6; + --fd-accent: #f97316; + --fd-accent-soft: rgba(249, 115, 22, 0.16); + --fd-warning: #f59e0b; + --fd-success: #10b981; + --fd-danger: #ef4444; + --fd-shadow: 0 30px 60px rgba(15, 23, 42, 0.12); + --fd-radius-xl: 28px; + --fd-radius-lg: 22px; + --fd-radius-md: 16px; + --fd-radius-sm: 12px; } + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + body { margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - text-align: center; + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + color: var(--fd-text); + background: + radial-gradient(circle at top left, rgba(20, 184, 166, 0.22), transparent 24%), + radial-gradient(circle at top right, rgba(249, 115, 22, 0.12), transparent 20%), + linear-gradient(160deg, #f8fbfc 0%, #edf4f6 45%, #e8eef4 100%); overflow: hidden; - position: relative; +} + +a { + color: inherit; + text-decoration: none; +} + +h1, +h2, +h3, +h4, +.brand-mark__core { + font-family: 'Manrope', 'Inter', sans-serif; + letter-spacing: -0.03em; +} + +.app-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 88px minmax(0, 1fr); +} + +.app-sidebar { + position: relative; + padding: 24px 18px; + background: rgba(10, 15, 25, 0.95); + display: flex; + flex-direction: column; + align-items: center; + gap: 28px; + border-right: 1px solid rgba(255, 255, 255, 0.06); +} + +.brand-mark { + width: 52px; + height: 52px; + border-radius: 18px; + display: grid; + place-items: center; + color: white; + background: linear-gradient(135deg, var(--fd-secondary), var(--fd-primary)); + box-shadow: 0 16px 30px rgba(20, 184, 166, 0.3); +} + +.brand-mark__core { + font-size: 1.35rem; + font-weight: 800; +} + +.sidebar-nav { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + align-items: center; +} + +.sidebar-link { + width: 52px; + height: 52px; + border-radius: 16px; + display: grid; + place-items: center; + color: rgba(255, 255, 255, 0.76); + font-size: 1.1rem; + position: relative; + transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease; +} + +.sidebar-link:hover, +.sidebar-link:focus-visible, +.sidebar-link.is-active { + color: #fff; + background: rgba(255, 255, 255, 0.1); + transform: translateY(-1px); + outline: none; +} + +.sidebar-link::after { + content: attr(data-label); + position: absolute; + left: 62px; + padding: 8px 12px; + border-radius: 999px; + background: rgba(10, 15, 25, 0.94); + color: #fff; + font-size: 0.83rem; + line-height: 1; + opacity: 0; + transform: translateX(-6px); + pointer-events: none; + transition: 0.2s ease; + white-space: nowrap; +} + +.sidebar-link:hover::after, +.sidebar-link:focus-visible::after { + opacity: 1; + transform: translateX(0); +} + +.app-main { + padding: 24px 28px 28px; + overflow: auto; + display: grid; + gap: 20px; + align-content: start; +} + +.hero-panel, +.surface-card, +.search-card, +.stat-card, +.detail-topbar { + border: 1px solid var(--fd-border); + box-shadow: var(--fd-shadow); +} + +.hero-panel { + position: relative; + overflow: hidden; + display: grid; + grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.9fr); + gap: 16px; + padding: 32px; + border-radius: var(--fd-radius-xl); + background: linear-gradient(135deg, rgba(15, 118, 110, 0.96), rgba(10, 15, 25, 0.98)); + color: white; +} + +.hero-panel::before { + content: ''; + position: absolute; + inset: auto auto -60px -60px; + width: 220px; + height: 220px; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.22), transparent 64%); +} + +.hero-copy { + position: relative; + z-index: 1; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.78rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--fd-secondary); + font-weight: 700; +} + +.hero-panel .eyebrow { + color: rgba(255, 255, 255, 0.72); +} + +.hero-copy h1, +.detail-topbar h1 { + margin: 10px 0 12px; + font-size: clamp(2rem, 3vw, 3.5rem); + line-height: 1.02; +} + +.hero-copy h1 span { + color: #a7f3d0; +} + +.hero-copy p, +.detail-topbar p, +.stat-card p, +.template-tile p, +.empty-state p, +.form-actions p, +.workspace-row__meta, +.agenda-item p, +.activity-row p { + color: rgba(255, 255, 255, 0.8); +} + +.hero-copy p { + max-width: 720px; + font-size: 1rem; + margin-bottom: 0; +} + +.hero-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 24px; +} + +.hero-visual { + position: relative; + min-height: 220px; +} + +.shape { + position: absolute; + border-radius: 28px; + backdrop-filter: blur(18px); +} + +.shape-sphere { + width: 142px; + height: 142px; + top: 6px; + right: 40px; + border-radius: 50%; + background: radial-gradient(circle at 32% 32%, rgba(255, 255, 255, 0.95), rgba(20, 184, 166, 0.18) 45%, rgba(255, 255, 255, 0.08) 72%); + box-shadow: inset -20px -20px 42px rgba(255, 255, 255, 0.06); +} + +.shape-card { + width: 240px; + height: 170px; + right: 20px; + bottom: 14px; + padding: 18px; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.18); +} + +.mini-bar { + height: 14px; + width: 110px; + border-radius: 999px; + margin-bottom: 20px; + background: linear-gradient(90deg, rgba(255,255,255,0.9), rgba(255,255,255,0.35)); +} + +.mini-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.mini-grid span { + display: block; + height: 46px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.14); +} + +.shape-cylinder { + width: 84px; + height: 150px; + left: 20px; + bottom: 4px; + border-radius: 999px; + background: linear-gradient(180deg, rgba(249, 115, 22, 0.95), rgba(249, 115, 22, 0.16)); +} + +.top-toolbar, +.detail-toolbar { + display: flex; + gap: 14px; + align-items: center; + justify-content: space-between; +} + +.search-card { + display: flex; + gap: 14px; + align-items: center; + border-radius: var(--fd-radius-lg); + background: rgba(255, 255, 255, 0.72); + padding: 12px; + flex: 1; +} + +.search-card--static { + padding: 10px 12px; +} + +.search-field-wrap { + flex: 1; + display: flex; + align-items: center; + gap: 12px; + padding: 0 10px; + border-radius: 14px; + background: rgba(15, 23, 42, 0.04); +} + +.search-field-wrap i { + color: var(--fd-muted); +} + +.search-field { + width: 100%; + border: 0; + padding: 12px 0; + background: transparent; + color: var(--fd-text); + font-size: 0.98rem; +} + +.search-field:focus { + outline: none; +} + +.admin-pill, +.section-chip, +.stage-pill { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 999px; + padding: 10px 14px; + font-size: 0.82rem; + font-weight: 700; +} + +.admin-pill { + background: rgba(15, 23, 42, 0.94); + color: #fff; +} + +.stats-grid, +.content-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 18px; +} + +.stat-card { + grid-column: span 3; + padding: 22px; + border-radius: 24px; + background: rgba(255, 255, 255, 0.72); +} + +.stat-card strong { + display: block; + margin: 10px 0 8px; + font-size: 2rem; + font-family: 'Manrope', sans-serif; +} + +.stat-label { + color: var(--fd-muted); + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.stat-card p, +.template-tile p, +.empty-state p, +.form-actions p, +.workspace-row__meta, +.agenda-item p, +.activity-row p, +.detail-topbar p, +.info-row span, +.summary-box { + color: var(--fd-muted); +} + +.stat-card--accent { + background: linear-gradient(135deg, rgba(249, 115, 22, 0.96), rgba(255, 149, 60, 0.94)); + color: white; +} + +.stat-card--accent .stat-label, +.stat-card--accent p { + color: rgba(255, 255, 255, 0.85); +} + +.surface-card { + padding: 24px; + border-radius: var(--fd-radius-xl); + background: var(--fd-surface); + backdrop-filter: blur(22px); +} + +.template-card-list { + grid-column: span 4; +} + +.create-card { + grid-column: span 8; +} + +.list-card { + grid-column: span 7; +} + +.agenda-card { + grid-column: span 5; +} + +.section-heading, +.panel-head { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; + margin-bottom: 18px; +} + +.section-heading h2, +.panel-head h2 { + margin: 6px 0 0; + font-size: 1.5rem; +} + +.section-chip { + background: rgba(20, 184, 166, 0.12); + color: var(--fd-primary); +} + +.template-list, +.agenda-list, +.activity-feed { + display: grid; + gap: 12px; +} + +.template-tile, +.agenda-item, +.activity-row, +.workspace-row { + display: flex; + gap: 14px; + align-items: center; + justify-content: space-between; + border-radius: 18px; + padding: 16px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(15, 23, 42, 0.06); +} + +.template-icon, +.agenda-icon { + width: 44px; + height: 44px; + border-radius: 14px; + display: grid; + place-items: center; + background: rgba(20, 184, 166, 0.12); + color: var(--fd-primary); + flex-shrink: 0; +} + +.workspace-form, +.compact-form { + display: grid; + gap: 18px; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 14px; +} + +.form-grid--quote .form-field { + grid-column: span 3; +} + +.form-field { + grid-column: span 4; + display: grid; + gap: 8px; +} + +.form-field--wide { + grid-column: span 6; +} + +.form-field--full { + grid-column: 1 / -1; +} + +.form-field label, +.compact-form__field label { + font-size: 0.84rem; + font-weight: 700; + color: var(--fd-text); +} + +.workspace-input { + border: 1px solid rgba(15, 23, 42, 0.1); + border-radius: 14px; + padding: 0.76rem 0.95rem; + background: rgba(255, 255, 255, 0.96); + color: var(--fd-text); +} + +.workspace-input:focus { + border-color: rgba(20, 184, 166, 0.8); + box-shadow: 0 0 0 0.24rem rgba(20, 184, 166, 0.14); +} + +.field-error { + color: var(--fd-danger); + font-size: 0.82rem; +} + +.form-actions { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: center; + flex-wrap: wrap; +} + +.form-actions--inline { + justify-content: flex-start; +} + +.btn { + border-radius: 14px; + border: none; + padding: 0.85rem 1.2rem; + font-weight: 700; + transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease; +} + +.btn:hover, +.btn:focus-visible { + transform: translateY(-1px); +} + +.btn-accent { + color: white; + background: linear-gradient(135deg, var(--fd-accent), #fb923c); + box-shadow: 0 16px 24px rgba(249, 115, 22, 0.24); +} + +.btn-ghost { + color: white; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.16); +} + +.btn-dark-shell { + color: white; + background: linear-gradient(135deg, #172033, #0f172a); + box-shadow: 0 10px 18px rgba(15, 23, 42, 0.18); +} + +.workspace-list { + display: grid; + gap: 12px; +} + +.workspace-row__title { + font-weight: 800; + font-size: 1.02rem; +} + +.workspace-row__side { + display: grid; + justify-items: end; + gap: 8px; + flex-shrink: 0; +} + +.empty-state { + min-height: 220px; + display: grid; + place-items: center; + text-align: center; + gap: 8px; + border-radius: 22px; + background: rgba(255, 255, 255, 0.58); + border: 1px dashed rgba(15, 23, 42, 0.14); + padding: 20px; +} + +.empty-state i { + font-size: 1.7rem; + color: var(--fd-primary); +} + +.empty-state--small { + min-height: 120px; +} + +.message-stack { + display: grid; + gap: 10px; +} + +.custom-alert { + margin: 0; + border-radius: 16px; + border: 0; + padding: 14px 16px; +} + +.alert-success { + background: rgba(16, 185, 129, 0.14); + color: #0d7a55; +} + +.alert-error { + background: rgba(239, 68, 68, 0.12); + color: #b91c1c; +} + +.detail-topbar { + display: flex; + justify-content: space-between; + gap: 18px; + align-items: end; + padding: 24px 28px; + border-radius: var(--fd-radius-xl); + background: rgba(255, 255, 255, 0.72); +} + +.back-link { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--fd-primary); + font-weight: 700; +} + +.detail-meta { + display: flex; + flex-direction: column; + gap: 10px; + align-items: flex-end; +} + +.detail-value { + font-family: 'Manrope', sans-serif; + font-size: 2.1rem; + font-weight: 800; +} + +.workspace-grid { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 18px; +} + +.panel { + min-height: 260px; + overflow: hidden; +} + +.panel-opportunity { + grid-column: span 4; +} + +.panel-projects { + grid-column: span 3; +} + +.panel-quote { + grid-column: span 5; +} + +.panel-agenda { + grid-column: span 4; +} + +.panel-activities { + grid-column: span 8; +} + +.template-quotation_focus .panel-quote { + grid-column: span 8; +} + +.template-quotation_focus .panel-projects { + grid-column: span 4; +} + +.template-planning .panel-agenda { + grid-column: span 5; +} + +.template-planning .panel-activities { + grid-column: span 7; +} + +.info-stack { + display: grid; + gap: 10px; +} + +.info-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 12px 14px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.72); +} + +.info-row strong { + text-align: right; +} + +.summary-box { + border-radius: 18px; + background: rgba(15, 118, 110, 0.08); + padding: 16px; +} + +.compact-form--inline { + grid-template-columns: 1fr auto; + align-items: end; +} + +.compact-form__field { + display: grid; + gap: 8px; +} + +.panel-action { + border: 0; + background: rgba(15, 23, 42, 0.04); + border-radius: 999px; + padding: 8px 12px; + color: var(--fd-muted); + font-weight: 700; +} + +.quote-table-wrap { + overflow: auto; + border-radius: 18px; + border: 1px solid rgba(15, 23, 42, 0.08); +} + +.quote-table { + margin: 0; +} + +.quote-table thead th { + background: rgba(15, 23, 42, 0.03); + color: var(--fd-muted); + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.08em; + border-bottom: 0; +} + +.quote-table tbody td { + vertical-align: middle; + color: var(--fd-text); + border-color: rgba(15, 23, 42, 0.06); +} + +.quote-total { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 4px 0; + font-weight: 700; +} + +.activity-row { + align-items: flex-start; +} + +.stage-pill { + padding: 8px 12px; +} + +.stage-new { + background: rgba(20, 184, 166, 0.12); + color: var(--fd-primary); +} + +.stage-qualified { + background: rgba(14, 165, 233, 0.14); + color: #0369a1; +} + +.stage-proposal { + background: rgba(245, 158, 11, 0.18); + color: #b45309; +} + +.stage-negotiation { + background: rgba(249, 115, 22, 0.18); + color: #c2410c; +} + +.stage-won { + background: rgba(16, 185, 129, 0.14); + color: #047857; +} + +.stage-lost { + background: rgba(239, 68, 68, 0.12); + color: #b91c1c; +} + +@media (max-width: 1199px) { + body { + overflow: auto; + } + + .stats-grid .stat-card, + .template-card-list, + .create-card, + .list-card, + .agenda-card, + .panel-opportunity, + .panel-projects, + .panel-quote, + .panel-agenda, + .panel-activities { + grid-column: span 12; + } +} + +@media (max-width: 991px) { + .app-shell { + grid-template-columns: 1fr; + } + + .app-sidebar { + position: sticky; + top: 0; + z-index: 20; + flex-direction: row; + justify-content: space-between; + padding: 16px 20px; + } + + .sidebar-nav { + flex-direction: row; + justify-content: center; + overflow: auto; + } + + .sidebar-link::after { + display: none; + } + + .hero-panel, + .detail-topbar { + grid-template-columns: 1fr; + } + + .top-toolbar, + .detail-toolbar, + .search-card, + .form-actions, + .compact-form--inline, + .detail-topbar { + flex-direction: column; + align-items: stretch; + } + + .form-field, + .form-field--wide, + .form-field--full, + .form-grid--quote .form-field { + grid-column: span 12; + } + + .app-main { + padding: 18px; + } +} + +@media (max-width: 575px) { + .hero-panel, + .surface-card, + .detail-topbar { + padding: 20px; + border-radius: 22px; + } + + .hero-copy h1, + .detail-topbar h1 { + font-size: 1.85rem; + } + + .sidebar-link { + width: 46px; + height: 46px; + } }