feat(social): Implement Real Meta API, Webhook Controller, Auto-Reply tests, AI Sentiment analysis and Security Rules

This commit is contained in:
cerenX9
2026-07-27 20:29:58 +03:00
parent 75c390d1a4
commit 36784f0433
73 changed files with 6324 additions and 77 deletions

View File

@@ -6,7 +6,7 @@ USER root
COPY requirements.txt /etc/odoo/
# Gerekli Python paketlerini kur
RUN pip3 install --no-cache-dir -r /etc/odoo/requirements.txt
RUN pip3 install --no-cache-dir --break-system-packages --ignore-installed -r /etc/odoo/requirements.txt
# Odoo kullanıcısına geri dön
USER odoo

View File

@@ -1 +1,3 @@
from . import models
from . import controllers
from . import wizard

View File

@@ -22,10 +22,32 @@ Features:
""",
'author': 'MyMach',
'website': 'https://www.mymach.com',
'depends': ['base', 'web', 'crm', 'mail'],
'depends': ['base', 'mail', 'social_media', 'utm', 'link_tracker', 'crm', 'base_setup', 'product'],
'data': [
'security/security.xml',
'security/ir_rule.xml',
'security/ir.model.access.csv',
'data/social_post_cron.xml',
'data/social_competitor_cron.xml',
'views/res_config_settings_views.xml',
'views/social_analytics_views.xml',
'views/social_live_post_views.xml',
'views/social_post_views.xml',
'wizard/social_ai_wizard_views.xml',
'views/social_stream_views.xml',
'views/social_media_views.xml',
'views/social_auto_reply_views.xml',
'views/social_auto_reply_log_views.xml',
'views/social_stream_post_kanban_views.xml',
'views/social_menu.xml',
'views/social_account_views.xml',
'views/social_competitor_views.xml',
],
'assets': {
'web.assets_backend': [
'mymach_social_nextgen/static/src/css/social_dashboard.css',
],
},
'installable': True,
'application': True,
'auto_install': False,

View File

@@ -0,0 +1,6 @@
from . import meta_api
from . import instagram_api
from . import linkedin_api
from . import x_api
from . import youtube_api
from . import tiktok_api

View File

@@ -0,0 +1,36 @@
import requests
from urllib.parse import urlencode
class InstagramAPI:
"""Helper class for direct Instagram OAuth (Basic Display or Native Auth)."""
AUTH_URL = "https://api.instagram.com/oauth/authorize"
TOKEN_URL = "https://api.instagram.com/oauth/access_token"
GRAPH_URL = "https://graph.instagram.com"
def __init__(self, client_id, client_secret, redirect_uri):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_auth_url(self, state):
params = {
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'state': state,
'scope': 'user_profile,user_media',
'response_type': 'code'
}
return f"{self.AUTH_URL}?{urlencode(params)}"
def exchange_code_for_token(self, code):
data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'grant_type': 'authorization_code',
'redirect_uri': self.redirect_uri,
'code': code
}
response = requests.post(self.TOKEN_URL, data=data, timeout=10)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,36 @@
import requests
from urllib.parse import urlencode
class LinkedInAPI:
"""Helper class for LinkedIn API."""
AUTH_URL = "https://www.linkedin.com/oauth/v2/authorization"
TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken"
def __init__(self, client_id, client_secret, redirect_uri):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_auth_url(self, state):
params = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'state': state,
'scope': 'w_member_social r_liteprofile w_organization_social r_organization_social'
}
return f"{self.AUTH_URL}?{urlencode(params)}"
def exchange_code_for_token(self, code):
data = {
'grant_type': 'authorization_code',
'code': code,
'client_id': self.client_id,
'client_secret': self.client_secret,
'redirect_uri': self.redirect_uri
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,69 @@
import requests
from urllib.parse import urlencode
class MetaAPI:
"""Helper class for Meta (Facebook/Instagram) Graph API."""
AUTH_URL = "https://www.facebook.com/v19.0/dialog/oauth"
TOKEN_URL = "https://graph.facebook.com/v19.0/oauth/access_token"
GRAPH_URL = "https://graph.facebook.com/v19.0"
def __init__(self, client_id, client_secret, redirect_uri):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_auth_url(self, state):
params = {
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'state': state,
'scope': 'pages_show_list,pages_read_engagement,pages_manage_posts,instagram_basic,instagram_content_publish',
'response_type': 'code'
}
return f"{self.AUTH_URL}?{urlencode(params)}"
def exchange_code_for_token(self, code):
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'redirect_uri': self.redirect_uri,
'code': code
}
response = requests.get(self.TOKEN_URL, params=params, timeout=10)
response.raise_for_status()
return response.json()
def get_profile_info(self, access_token):
"""Kullanıcının yönettiği sayfaları ve hesap ID'lerini çeker."""
url = f"{self.GRAPH_URL}/me/accounts"
params = {'access_token': access_token}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
return response.json()
def publish_post(self, page_id, access_token, message, link=None):
"""Belirtilen sayfada gönderi paylaşır."""
url = f"{self.GRAPH_URL}/{page_id}/feed"
payload = {
'message': message,
'access_token': access_token
}
if link:
payload['link'] = link
response = requests.post(url, data=payload, timeout=15)
response.raise_for_status()
return response.json()
def refresh_token(self, short_lived_token):
"""Kısa ömürlü token'ı uzun ömürlü (60 günlük) token'a çevirir veya süresini uzatır."""
params = {
'grant_type': 'fb_exchange_token',
'client_id': self.client_id,
'client_secret': self.client_secret,
'fb_exchange_token': short_lived_token
}
response = requests.get(self.TOKEN_URL, params=params, timeout=10)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,38 @@
import requests
from urllib.parse import urlencode
class TikTokAPI:
"""Helper class for TikTok Developer API / OAuth 2.0."""
AUTH_URL = "https://www.tiktok.com/v2/auth/authorize/"
TOKEN_URL = "https://open.tiktokapis.com/v2/oauth/token/"
def __init__(self, client_id, client_secret, redirect_uri):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_auth_url(self, state):
params = {
'client_key': self.client_id,
'redirect_uri': self.redirect_uri,
'state': state,
'scope': 'user.info.basic,video.list,video.upload',
'response_type': 'code'
}
return f"{self.AUTH_URL}?{urlencode(params)}"
def exchange_code_for_token(self, code):
data = {
'client_key': self.client_id,
'client_secret': self.client_secret,
'grant_type': 'authorization_code',
'redirect_uri': self.redirect_uri,
'code': code
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,45 @@
import requests
import base64
from urllib.parse import urlencode
class XAPI:
"""Helper class for X (Twitter) OAuth 2.0 API."""
AUTH_URL = "https://twitter.com/i/oauth2/authorize"
TOKEN_URL = "https://api.twitter.com/2/oauth2/token"
def __init__(self, client_id, client_secret, redirect_uri):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_auth_url(self, state, code_challenge="challenge"):
# Note: X requires PKCE. For a complete implementation, a real code_verifier and code_challenge should be generated.
params = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'scope': 'tweet.read tweet.write users.read offline.access',
'state': state,
'code_challenge': code_challenge,
'code_challenge_method': 'plain'
}
return f"{self.AUTH_URL}?{urlencode(params)}"
def exchange_code_for_token(self, code, code_verifier="challenge"):
data = {
'grant_type': 'authorization_code',
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'code': code,
'code_verifier': code_verifier
}
auth_string = f"{self.client_id}:{self.client_secret}"
b64_auth = base64.b64encode(auth_string.encode('ascii')).decode('ascii')
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': f'Basic {b64_auth}'
}
response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,37 @@
import requests
from urllib.parse import urlencode
class YouTubeAPI:
"""Helper class for Google/YouTube Data API v3."""
AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
TOKEN_URL = "https://oauth2.googleapis.com/token"
def __init__(self, client_id, client_secret, redirect_uri):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_auth_url(self, state):
params = {
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'response_type': 'code',
'scope': 'https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube.readonly',
'access_type': 'offline',
'prompt': 'consent',
'state': state
}
return f"{self.AUTH_URL}?{urlencode(params)}"
def exchange_code_for_token(self, code):
data = {
'grant_type': 'authorization_code',
'code': code,
'client_id': self.client_id,
'client_secret': self.client_secret,
'redirect_uri': self.redirect_uri
}
response = requests.post(self.TOKEN_URL, data=data, timeout=10)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,2 @@
from . import main
from . import webhook_controller

View File

@@ -0,0 +1,76 @@
from odoo import http
from odoo.http import request
import werkzeug
class SocialMediaController(http.Controller):
@http.route('/api/social/callback/<string:platform>', type='http', auth='user', website=True)
def social_oauth_callback(self, platform, **kw):
"""
OAuth2 callback from social platforms.
Expected parameters usually include 'code' and sometimes 'state' (which can contain our account ID).
"""
code = kw.get('code')
state = kw.get('state')
error = kw.get('error')
if error:
return request.render('http_routing.http_error', {'status_code': 400, 'status_message': f'OAuth Error: {error}'})
if not code:
return request.render('http_routing.http_error', {'status_code': 400, 'status_message': 'No authorization code received.'})
# Typically, state contains the ID of the social.account record we are trying to link.
account_id = None
try:
if state:
account_id = int(state)
except ValueError:
pass
if not account_id:
return "Hata: State parametresi (account_id) bulunamadı."
account = request.env['mymach.social.account'].browse(account_id)
if not account.exists():
return request.render('http_routing.http_error', {'status_code': 404, 'status_message': 'Social account record not found.'})
# Process the code based on the platform
try:
account.process_oauth_callback(code)
# Redirect back to the account form view
action = request.env.ref('mymach_social_nextgen.action_social_account')
url = f"/web#id={account.id}&model=mymach.social.account&view_type=form&action={action.id}"
return werkzeug.utils.redirect(url)
except Exception as e:
return request.render('http_routing.http_error', {'status_code': 500, 'status_message': f'Error processing callback: {str(e)}'})
@http.route(['/social/meta/webhook'], type='json', auth="public", methods=['GET', 'POST'], csrf=False)
def meta_webhook(self, **post):
"""
Endpoint to receive real-time updates from Meta (Instagram/Facebook).
Supports GET for verification, POST for receiving data.
"""
if request.httprequest.method == 'GET':
# Meta verification logic
verify_token = request.env['ir.config_parameter'].sudo().get_param('mymach_social.meta_verify_token')
mode = request.params.get('hub.mode')
token = request.params.get('hub.verify_token')
challenge = request.params.get('hub.challenge')
if mode and token:
if mode == 'subscribe' and token == verify_token:
return werkzeug.wrappers.Response(challenge, status=200)
else:
return werkzeug.wrappers.Response('Forbidden', status=403)
return werkzeug.wrappers.Response('Invalid Request', status=400)
elif request.httprequest.method == 'POST':
# Receive Webhook Payload (Comments, Messages, Likes)
data = request.jsonrequest
# Normalde burada data parse edilir ve mymach.social.stream.post kayıtları oluşturulur.
# Şimdilik canlı olmadığı için logluyoruz.
# _logger.info("Meta Webhook Received: %s", data)
return {'status': 'success'}

View File

@@ -0,0 +1,74 @@
from odoo import http
from odoo.http import request
import json
import logging
_logger = logging.getLogger(__name__)
class MetaWebhookController(http.Controller):
@http.route('/api/social/webhook/meta', type='http', auth='public', methods=['GET', 'POST'], csrf=False)
def meta_webhook(self, **kw):
"""
Meta (Facebook/Instagram) Webhook Endpoint.
GET isteği webhook verification (doğrulama) içindir.
POST isteği ise gerçek zamanlı yorum veya mesaj olaylarını (events) alır.
"""
if request.httprequest.method == 'GET':
# Verification for Meta Webhook setup
verify_token = request.env['ir.config_parameter'].sudo().get_param('mymach_social.meta_webhook_token', 'default_mymach_token')
mode = kw.get('hub.mode')
token = kw.get('hub.verify_token')
challenge = kw.get('hub.challenge')
if mode == 'subscribe' and token == verify_token:
_logger.info("Meta Webhook başarıyla doğrulandı.")
return request.make_response(challenge, headers=[('Content-Type', 'text/plain')])
else:
_logger.warning("Meta Webhook doğrulama hatası!")
return request.make_response('Verification failed', status=403)
elif request.httprequest.method == 'POST':
# Handle incoming webhook event
try:
data = json.loads(request.httprequest.data)
_logger.info(f"Meta Webhook veri alındı: {data}")
if data.get('object') == 'page':
for entry in data.get('entry', []):
page_id = entry.get('id')
# Hesabı (Sayfayı) bul
account = request.env['mymach.social.account'].sudo().search([('platform_account_id', '=', page_id), ('platform', '=', 'facebook')], limit=1)
if not account:
_logger.warning(f"Hesap bulunamadı (Page ID: {page_id})")
continue
# İlgili stream'i bul (Feed stream)
stream = request.env['mymach.social.stream'].sudo().search([('account_id', '=', account.id), ('stream_type', '=', 'feed')], limit=1)
if not stream:
continue
for messaging_event in entry.get('changes', []):
value = messaging_event.get('value', {})
item = value.get('item')
# Sadece yorumları işle (basit örnek)
if item == 'comment' and value.get('verb') == 'add':
message_text = value.get('message')
author_name = value.get('from', {}).get('name', 'Bilinmeyen Kullanıcı')
post_id = value.get('post_id')
# Odoo'da yeni Stream Post oluştur (bu otomatik yanıtları da tetikler)
request.env['mymach.social.stream.post'].sudo().create({
'stream_id': stream.id,
'author_name': author_name,
'message': message_text,
'message_type': 'comment',
'platform_post_id': value.get('comment_id')
})
return request.make_response('EVENT_RECEIVED', status=200)
except Exception as e:
_logger.error(f"Meta Webhook işlenirken hata oluştu: {str(e)}")
return request.make_response('Error processing webhook', status=500)

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_cron_scrape_competitors" model="ir.cron">
<field name="name">Sosyal Medya: Rakip Analizi Veri Kazıma</field>
<field name="model_id" ref="model_social_competitor"/>
<field name="state">code</field>
<field name="code">model._cron_scrape_competitors()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Cron Job to Publish Scheduled Posts -->
<record id="ir_cron_publish_scheduled_posts" model="ir.cron">
<field name="name">Social Media: Publish Scheduled Posts</field>
<field name="model_id" ref="model_mymach_social_post"/>
<field name="state">code</field>
<field name="code">model._cron_publish_scheduled_posts()</field>
<field name="interval_number">15</field>
<field name="interval_type">minutes</field>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,517 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mymach_social_nextgen
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 19.0-20260723\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-27 10:14+0000\n"
"PO-Revision-Date: 2026-07-27 10:14+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__is_ai_generated
msgid "AI Generated"
msgstr "Yapay Zeka ile Üretildi"
#. module: mymach_social_nextgen
#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_form
msgid "API Settings"
msgstr "API Ayarları"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__access_token
msgid "Access Token"
msgstr "Erişim Jetonu"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__name
msgid "Account Name"
msgstr "Hesap Adı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_needaction
msgid "Action Needed"
msgstr "Eylem Gerekiyor"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__active
msgid "Active"
msgstr "Aktif"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_ids
msgid "Activities"
msgstr "Aktiviteler"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_exception_decoration
msgid "Activity Exception Decoration"
msgstr "Aktivite İstisnası Dekorasyonu"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_state
msgid "Activity State"
msgstr "Aktivite Durumu"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_type_icon
msgid "Activity Type Icon"
msgstr "Aktivite Türü İkonu"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__notes
msgid "Analysis Notes"
msgstr "Analiz Notları"
#. module: mymach_social_nextgen
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_analytics
msgid "Analytics"
msgstr "Analiz"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_attachment_count
msgid "Attachment Count"
msgstr "Ek Sayısı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__name
msgid "Competitor Name"
msgstr "Rakip Adı"
#. module: mymach_social_nextgen
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_configuration
msgid "Configuration"
msgstr "Yapılandırma"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__content
msgid "Content"
msgstr "İçerik"
#. module: mymach_social_nextgen
#: model_terms:ir.actions.act_window,help:mymach_social_nextgen.action_social_account
msgid "Create your first Social Media Account connection!"
msgstr "İlk Sosyal Medya Hesabı bağlantınızı oluşturun!"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__create_uid
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__create_uid
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__create_uid
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__create_uid
msgid "Created by"
msgstr "Oluşturan"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__create_date
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__create_date
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__create_date
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__create_date
msgid "Created on"
msgstr "Oluşturulma Tarihi"
#. module: mymach_social_nextgen
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_dashboard
msgid "Dashboard"
msgstr "Kontrol Paneli"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__display_name
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__display_name
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__display_name
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__display_name
msgid "Display Name"
msgstr "Görünen Ad"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__draft
msgid "Draft"
msgstr "Taslak"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_media__media_type__template
msgid "Dynamic Template"
msgstr "Dinamik Şablon"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__engagement_rate
msgid "Engagement Rate (%)"
msgstr "Etkileşim Oranı (%)"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__error_message
msgid "Error Message"
msgstr "Hata Mesajı"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__facebook
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__facebook
msgid "Facebook"
msgstr "Facebook"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__failed
msgid "Failed"
msgstr "Başarısız"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__file_data
msgid "File"
msgstr "Dosya"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__name
msgid "File Name"
msgstr "Dosya Adı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_follower_ids
msgid "Followers"
msgstr "Takipçiler"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_partner_ids
msgid "Followers (Partners)"
msgstr "Takipçiler (İş Ortakları)"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__followers_count
msgid "Followers Count"
msgstr "Takipçi Sayısı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_type_icon
msgid "Font awesome icon e.g. fa-tasks"
msgstr "Font awesome ikonu örn. fa-tasks"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__has_message
msgid "Has Message"
msgstr "Mesaj Var"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__id
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__id
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__id
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__id
msgid "ID"
msgstr "ID"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_exception_icon
msgid "Icon"
msgstr "İkon"
#. module: mymach_social_nextgen
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_exception_icon
msgid "Icon to indicate an exception activity."
msgstr "Bir istisna aktivitesini gösteren ikon."
#. module: mymach_social_nextgen
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_needaction
msgid "If checked, new messages require your attention."
msgstr "İşaretliyse, yeni mesajlar dikkatinizi gerektirir."
#. module: mymach_social_nextgen
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_has_error
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_has_sms_error
msgid "If checked, some messages have a delivery error."
msgstr "İşaretliyse, bazı mesajlarda teslimat hatası var."
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_media__media_type__image
msgid "Image"
msgstr "Görsel"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__instagram
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__instagram
msgid "Instagram"
msgstr "Instagram"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_is_follower
msgid "Is Follower"
msgstr "Takipçi mi"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__last_scraped
msgid "Last Scraped Date"
msgstr "Son Taranma Tarihi"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__write_uid
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__write_uid
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__write_uid
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__write_uid
msgid "Last Updated by"
msgstr "Son Güncelleyen"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__write_date
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__write_date
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__write_date
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__write_date
msgid "Last Updated on"
msgstr "Son Güncellenme Tarihi"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__linkedin
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__linkedin
msgid "LinkedIn"
msgstr "LinkedIn"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__media_ids
msgid "Media Files"
msgstr "Medya Dosyaları"
#. module: mymach_social_nextgen
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_media
msgid "Media Library"
msgstr "Medya Kütüphanesi"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__media_type
msgid "Media Type"
msgstr "Medya Türü"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_has_error
msgid "Message Delivery error"
msgstr "Mesaj Teslimat hatası"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_ids
msgid "Messages"
msgstr "Mesajlar"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__mimetype
msgid "Mime Type"
msgstr "Mime Türü"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__my_activity_date_deadline
msgid "My Activity Deadline"
msgstr "Aktivite Son Tarihim"
#. module: mymach_social_nextgen
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_root
msgid "MyMach Social"
msgstr "MyMach Social"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_calendar_event_id
msgid "Next Activity Calendar Event"
msgstr "Sonraki Aktivite Takvim Etkinliği"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_date_deadline
msgid "Next Activity Deadline"
msgstr "Sonraki Aktivite Son Tarihi"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_summary
msgid "Next Activity Summary"
msgstr "Sonraki Aktivite Özeti"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_type_id
msgid "Next Activity Type"
msgstr "Sonraki Aktivite Türü"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_needaction_counter
msgid "Number of Actions"
msgstr "Eylem Sayısı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_has_error_counter
msgid "Number of errors"
msgstr "Hata sayısı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_needaction_counter
msgid "Number of messages requiring action"
msgstr "Eylem gerektiren mesaj sayısı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_has_error_counter
msgid "Number of messages with delivery error"
msgstr "Teslimat hatası olan mesaj sayısı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__platform
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__platform
msgid "Platform"
msgstr "Platform"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__platform_account_id
msgid "Platform Account ID"
msgstr "Platform Hesap ID"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__name
msgid "Post Title"
msgstr "Gönderi Başlığı"
#. module: mymach_social_nextgen
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_posts
msgid "Posts"
msgstr "Gönderiler"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__profile_image_url
msgid "Profile Image URL"
msgstr "Profil Resmi URL"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__profile_url
msgid "Profile URL"
msgstr "Profil URL"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__published
msgid "Published"
msgstr "Yayınlandı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__published_date
msgid "Published Date"
msgstr "Yayınlanma Tarihi"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__refresh_token
msgid "Refresh Token"
msgstr "Yenileme Jetonu"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_user_id
msgid "Responsible User"
msgstr "Sorumlu Kullanıcı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_has_sms_error
msgid "SMS Delivery error"
msgstr "SMS Teslimat hatası"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__scheduled
msgid "Scheduled"
msgstr "Planlandı"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__scheduled_date
msgid "Scheduled Date"
msgstr "Planlanma Tarihi"
#. module: mymach_social_nextgen
#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_form
msgid "Social Account"
msgstr "Sosyal Hesap"
#. module: mymach_social_nextgen
#: model:ir.actions.act_window,name:mymach_social_nextgen.action_social_account
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__account_ids
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_accounts
#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_tree
msgid "Social Accounts"
msgstr "Sosyal Hesaplar"
#. module: mymach_social_nextgen
#: model:ir.model,name:mymach_social_nextgen.model_social_account
msgid "Social Media Account"
msgstr "Sosyal Medya Hesabı"
#. module: mymach_social_nextgen
#: model:ir.model,name:mymach_social_nextgen.model_social_competitor
msgid "Social Media Competitor Analysis"
msgstr "Sosyal Medya Rakip Analizi"
#. module: mymach_social_nextgen
#: model:ir.model,name:mymach_social_nextgen.model_social_media
msgid "Social Media Library"
msgstr "Sosyal Medya Kütüphanesi"
#. module: mymach_social_nextgen
#: model:ir.model,name:mymach_social_nextgen.model_social_post
msgid "Social Media Post"
msgstr "Sosyal Medya Gönderisi"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__template_id
msgid "Source Template"
msgstr "Kaynak Şablon"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__state
msgid "Status"
msgstr "Durum"
#. module: mymach_social_nextgen
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_state
msgid ""
"Status based on activities\n"
"Overdue: Due date is already passed\n"
"Today: Activity date is today\n"
"Planned: Future activities."
msgstr ""
"Aktivitelere dayalı durum\n"
"Gecikmiş: Bitiş tarihi çoktan geçti\n"
"Bugün: Aktivite tarihi bugün\n"
"Planlanan: Gelecekteki aktiviteler."
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__token_expiration
msgid "Token Expiration Date"
msgstr "Jeton Bitiş Tarihi"
#. module: mymach_social_nextgen
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_exception_decoration
msgid "Type of the exception activity on record."
msgstr "Kayıttaki istisna aktivitesinin türü."
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_media__media_type__video
msgid "Video"
msgstr "Video"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__approval
msgid "Waiting for Approval"
msgstr "Onay Bekliyor"
#. module: mymach_social_nextgen
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__website_message_ids
msgid "Website Messages"
msgstr "Web Sitesi Mesajları"
#. module: mymach_social_nextgen
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__website_message_ids
msgid "Website communication history"
msgstr "Web sitesi iletişim geçmişi"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__x
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__x
msgid "X (Twitter)"
msgstr "X (Twitter)"
#. module: mymach_social_nextgen
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__youtube
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__youtube
msgid "YouTube"
msgstr "YouTube"
#. module: mymach_social_nextgen
#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_form
msgid "e.g. MyMach Official Facebook"
msgstr "örn. MyMach Resmi Facebook"

View File

@@ -0,0 +1,113 @@
import re
translations = {
"AI Generated": "Yapay Zeka ile Üretildi",
"API Settings": "API Ayarları",
"Access Token": "Erişim Jetonu",
"Account Name": "Hesap Adı",
"Action Needed": "Eylem Gerekiyor",
"Active": "Aktif",
"Activities": "Aktiviteler",
"Activity Exception Decoration": "Aktivite İstisnası Dekorasyonu",
"Activity State": "Aktivite Durumu",
"Activity Type Icon": "Aktivite Türü İkonu",
"Analysis Notes": "Analiz Notları",
"Analytics": "Analiz",
"Attachment Count": "Ek Sayısı",
"Competitor Name": "Rakip Adı",
"Configuration": "Yapılandırma",
"Content": "İçerik",
"Create your first Social Media Account connection!": "İlk Sosyal Medya Hesabı bağlantınızı oluşturun!",
"Created by": "Oluşturan",
"Created on": "Oluşturulma Tarihi",
"Dashboard": "Kontrol Paneli",
"Display Name": "Görünen Ad",
"Draft": "Taslak",
"Dynamic Template": "Dinamik Şablon",
"Engagement Rate (%)": "Etkileşim Oranı (%)",
"Error Message": "Hata Mesajı",
"Facebook": "Facebook",
"Failed": "Başarısız",
"File": "Dosya",
"File Name": "Dosya Adı",
"Followers": "Takipçiler",
"Followers (Partners)": "Takipçiler (İş Ortakları)",
"Followers Count": "Takipçi Sayısı",
"Font awesome icon e.g. fa-tasks": "Font awesome ikonu örn. fa-tasks",
"Has Message": "Mesaj Var",
"ID": "ID",
"Icon": "İkon",
"Icon to indicate an exception activity.": "Bir istisna aktivitesini gösteren ikon.",
"If checked, new messages require your attention.": "İşaretliyse, yeni mesajlar dikkatinizi gerektirir.",
"If checked, some messages have a delivery error.": "İşaretliyse, bazı mesajlarda teslimat hatası var.",
"Image": "Görsel",
"Instagram": "Instagram",
"Is Follower": "Takipçi mi",
"Last Scraped Date": "Son Taranma Tarihi",
"Last Updated by": "Son Güncelleyen",
"Last Updated on": "Son Güncellenme Tarihi",
"LinkedIn": "LinkedIn",
"Media Files": "Medya Dosyaları",
"Media Library": "Medya Kütüphanesi",
"Media Type": "Medya Türü",
"Message Delivery error": "Mesaj Teslimat hatası",
"Messages": "Mesajlar",
"Mime Type": "Mime Türü",
"My Activity Deadline": "Aktivite Son Tarihim",
"MyMach Social": "MyMach Social",
"Next Activity Calendar Event": "Sonraki Aktivite Takvim Etkinliği",
"Next Activity Deadline": "Sonraki Aktivite Son Tarihi",
"Next Activity Summary": "Sonraki Aktivite Özeti",
"Next Activity Type": "Sonraki Aktivite Türü",
"Number of Actions": "Eylem Sayısı",
"Number of errors": "Hata sayısı",
"Number of messages requiring action": "Eylem gerektiren mesaj sayısı",
"Number of messages with delivery error": "Teslimat hatası olan mesaj sayısı",
"Platform": "Platform",
"Platform Account ID": "Platform Hesap ID",
"Post Title": "Gönderi Başlığı",
"Posts": "Gönderiler",
"Profile Image URL": "Profil Resmi URL",
"Profile URL": "Profil URL",
"Published": "Yayınlandı",
"Published Date": "Yayınlanma Tarihi",
"Refresh Token": "Yenileme Jetonu",
"Responsible User": "Sorumlu Kullanıcı",
"SMS Delivery error": "SMS Teslimat hatası",
"Scheduled": "Planlandı",
"Scheduled Date": "Planlanma Tarihi",
"Social Account": "Sosyal Hesap",
"Social Accounts": "Sosyal Hesaplar",
"Social Media Account": "Sosyal Medya Hesabı",
"Social Media Competitor Analysis": "Sosyal Medya Rakip Analizi",
"Social Media Library": "Sosyal Medya Kütüphanesi",
"Social Media Post": "Sosyal Medya Gönderisi",
"Source Template": "Kaynak Şablon",
"Status": "Durum",
"Status based on activities\\nOverdue: Due date is already passed\\nToday: Activity date is today\\nPlanned: Future activities.": "Aktivitelere dayalı durum\\nGecikmiş: Bitiş tarihi çoktan geçti\\nBugün: Aktivite tarihi bugün\\nPlanlanan: Gelecekteki aktiviteler.",
"Token Expiration Date": "Jeton Bitiş Tarihi",
"Type of the exception activity on record.": "Kayıttaki istisna aktivitesinin türü.",
"Video": "Video",
"Waiting for Approval": "Onay Bekliyor",
"Website Messages": "Web Sitesi Mesajları",
"Website communication history": "Web sitesi iletişim geçmişi",
"X (Twitter)": "X (Twitter)",
"YouTube": "YouTube",
"e.g. MyMach Official Facebook": "örn. MyMach Resmi Facebook"
}
file_path = "c:/Users/Ortak/Desktop/mymach_social_nextgen/extra-addons/mymach_social_nextgen/i18n/tr.po"
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
for eng, tr in translations.items():
# Replacing single-line msgid
pattern = r'msgid "' + re.escape(eng) + r'"\nmsgstr ""'
replacement = 'msgid "' + eng + '"\nmsgstr "' + tr + '"'
content = re.sub(pattern, replacement, content)
# Manual replacement for multi-line status
content = content.replace('msgid ""\n"Status based on activities\\n"\n"Overdue: Due date is already passed\\n"\n"Today: Activity date is today\\n"\n"Planned: Future activities."\nmsgstr ""', 'msgid ""\n"Status based on activities\\n"\n"Overdue: Due date is already passed\\n"\n"Today: Activity date is today\\n"\n"Planned: Future activities."\nmsgstr ""\n"Aktivitelere dayalı durum\\n"\n"Gecikmiş: Bitiş tarihi çoktan geçti\\n"\n"Bugün: Aktivite tarihi bugün\\n"\n"Planlanan: Gelecekteki aktiviteler."')
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)

View File

@@ -1,4 +1,11 @@
from . import social_account
from . import social_post
from . import social_media
from . import social_live_post
from . import social_stream
from . import social_stream_post
from . import social_competitor
from . import res_config_settings
from . import res_company
from . import social_media
from . import social_auto_reply
from . import social_auto_reply_log

View File

@@ -0,0 +1,13 @@
from odoo import models, fields
class ResCompany(models.Model):
_inherit = 'res.company'
social_watermark_image = fields.Image(string="Filigran Logosu (PNG)", max_width=1024, max_height=1024)
social_watermark_position = fields.Selection([
('bottom_right', 'Sağ Alt'),
('bottom_left', 'Sol Alt'),
('top_right', 'Sağ Üst'),
('top_left', 'Sol Üst'),
('center', 'Merkez')
], string="Filigran Konumu", default='bottom_right')

View File

@@ -0,0 +1,29 @@
from odoo import models, fields
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
# Meta (Facebook/Instagram) API
social_meta_client_id = fields.Char(string="Meta Client ID", config_parameter="mymach_social.meta_client_id")
social_meta_client_secret = fields.Char(string="Meta Client Secret", config_parameter="mymach_social.meta_client_secret")
# LinkedIn API
social_linkedin_client_id = fields.Char(string="LinkedIn Client ID", config_parameter="mymach_social.linkedin_client_id")
social_linkedin_client_secret = fields.Char(string="LinkedIn Client Secret", config_parameter="mymach_social.linkedin_client_secret")
# X (Twitter) API
social_x_client_id = fields.Char(string="X (Twitter) Client ID", config_parameter="mymach_social.x_client_id")
social_x_client_secret = fields.Char(string="X (Twitter) Client Secret", config_parameter="mymach_social.x_client_secret")
# YouTube API
social_youtube_client_id = fields.Char(string="YouTube Client ID", config_parameter="mymach_social.youtube_client_id")
social_youtube_client_secret = fields.Char(string="YouTube Client Secret", config_parameter="mymach_social.youtube_client_secret")
# AI APIs
social_gemini_api_key = fields.Char(string="Google Gemini API Key", config_parameter="mymach_social.gemini_api_key")
social_openai_api_key = fields.Char(string="OpenAI API Key", config_parameter="mymach_social.openai_api_key")
social_claude_api_key = fields.Char(string="Claude API Key", config_parameter="mymach_social.claude_api_key")
social_grok_api_key = fields.Char(string="Grok API Key", config_parameter="mymach_social.grok_api_key")
# Watermark Settings
social_watermark_image = fields.Image(related='company_id.social_watermark_image', readonly=False)
social_watermark_position = fields.Selection(related='company_id.social_watermark_position', readonly=False)

View File

@@ -1,31 +1,144 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
from werkzeug.urls import url_join
from ..api.meta_api import MetaAPI
from ..api.instagram_api import InstagramAPI
from ..api.linkedin_api import LinkedInAPI
from ..api.x_api import XAPI
from ..api.youtube_api import YouTubeAPI
from ..api.tiktok_api import TikTokAPI
class SocialAccount(models.Model):
_name = 'social.account'
_name = 'mymach.social.account'
_description = 'Social Media Account'
name = fields.Char(string='Account Name', required=True)
name = fields.Char(string='Hesap Adı', required=True)
platform = fields.Selection([
('facebook', 'Facebook'),
('instagram', 'Instagram'),
('twitter', 'X (Twitter)'),
('linkedin', 'LinkedIn'),
('x', 'X (Twitter)'),
('youtube', 'YouTube')
('youtube', 'YouTube'),
('tiktok', 'TikTok'),
('web_push', 'Web Push Bildirimleri')
], string='Platform', required=True)
active = fields.Boolean(default=True)
access_token = fields.Char(string='Access Token', groups="base.group_system")
refresh_token = fields.Char(string='Refresh Token', groups="base.group_system")
token_expiration = fields.Datetime(string='Token Expiration Date')
active = fields.Boolean(string='Aktif', default=True)
access_token = fields.Char(string='Erişim Anahtarı (Access Token)', groups="base.group_system")
refresh_token = fields.Char(string='Yenileme Anahtarı (Refresh Token)', groups="base.group_system")
token_expiration = fields.Datetime(string='Token Geçerlilik Tarihi')
# Optional fields for user info
platform_account_id = fields.Char(string='Platform Account ID')
profile_image_url = fields.Char(string='Profile Image URL')
platform_account_id = fields.Char(string='Platform Hesap ID')
profile_image_url = fields.Char(string='Profil Resmi URL')
color = fields.Integer(string='Renk İndeksi')
# İstatistikler (Audience / Kitle)
audience_count = fields.Integer(string='Takipçi / Kitle', default=0, help="Toplam takipçi veya sayfa beğenisi sayısı")
engagement_rate = fields.Float(string='Etkileşim Oranı (%)', default=0.0)
def _get_api_client(self):
self.ensure_one()
config = self.env['ir.config_parameter'].sudo()
base_url = config.get_param('web.base.url')
redirect_uri = url_join(base_url, f'/api/social/callback/{self.platform}')
if self.platform == 'facebook':
client_id = config.get_param('mymach_social.meta_client_id')
client_secret = config.get_param('mymach_social.meta_client_secret')
if not client_id or not client_secret:
raise UserError("Meta API Client ID or Secret is missing in Settings.")
return MetaAPI(client_id, client_secret, redirect_uri)
elif self.platform == 'instagram':
client_id = config.get_param('mymach_social.meta_client_id')
client_secret = config.get_param('mymach_social.meta_client_secret')
if not client_id or not client_secret:
raise UserError("Instagram API Client ID or Secret is missing in Settings.")
return InstagramAPI(client_id, client_secret, redirect_uri)
elif self.platform == 'linkedin':
client_id = config.get_param('mymach_social.linkedin_client_id')
client_secret = config.get_param('mymach_social.linkedin_client_secret')
if not client_id or not client_secret:
raise UserError("LinkedIn API Client ID or Secret is missing in Settings.")
return LinkedInAPI(client_id, client_secret, redirect_uri)
elif self.platform == 'x':
client_id = config.get_param('mymach_social.x_client_id')
client_secret = config.get_param('mymach_social.x_client_secret')
if not client_id or not client_secret:
raise UserError("X (Twitter) API Client ID or Secret is missing in Settings.")
return XAPI(client_id, client_secret, redirect_uri)
elif self.platform == 'youtube':
client_id = config.get_param('mymach_social.youtube_client_id')
client_secret = config.get_param('mymach_social.youtube_client_secret')
if not client_id or not client_secret:
raise UserError("YouTube API Client ID or Secret is missing in Settings.")
return YouTubeAPI(client_id, client_secret, redirect_uri)
elif self.platform == 'tiktok':
client_id = config.get_param('mymach_social.tiktok_client_id')
client_secret = config.get_param('mymach_social.tiktok_client_secret')
if not client_id or not client_secret:
raise UserError("TikTok API Client ID or Secret is missing in Settings.")
return TikTokAPI(client_id, client_secret, redirect_uri)
raise UserError("Unsupported platform.")
def action_authenticate(self):
# Placeholder for OAuth2 authentication flow
pass
self.ensure_one()
api_client = self._get_api_client()
auth_url = api_client.get_auth_url(state=str(self.id))
return {
'type': 'ir.actions.act_url',
'url': auth_url,
'target': 'self'
}
def process_oauth_callback(self, code):
self.ensure_one()
api_client = self._get_api_client()
token_data = api_client.exchange_code_for_token(code)
# Save tokens
self.access_token = token_data.get('access_token')
if 'refresh_token' in token_data:
self.refresh_token = token_data.get('refresh_token')
# Optional: Calculate token expiration if expires_in is provided
# expires_in = token_data.get('expires_in')
# In a real app, we would also fetch user profile data here.
def action_refresh_token(self):
# Placeholder for refreshing the OAuth2 token
pass
self.ensure_one()
if self.platform == 'facebook' and self.access_token:
try:
api_client = self._get_api_client()
resp = api_client.refresh_token(self.access_token)
new_token = resp.get('access_token')
if new_token:
self.write({
'access_token': new_token,
'is_active': True,
})
# Odoo'nun standart success mesajı gösterilebilir
except Exception as e:
raise UserError(f"Token yenilenirken hata oluştu: {str(e)}")
else:
raise UserError("Bu platform için otomatik token yenileme desteklenmiyor.")
@api.model
def _cron_refresh_tokens(self):
# 60 gün dolmadan yenilenmesi gerekenleri bul (şimdilik tüm aktif facebook hesaplarını deneyelim)
accounts = self.search([('is_active', '=', True), ('platform', '=', 'facebook')])
for acc in accounts:
try:
acc.action_refresh_token()
except Exception as e:
# Log error and continue with others
pass

View File

@@ -0,0 +1,44 @@
from odoo import models, fields, api
class SocialAutoReplyRule(models.Model):
_name = 'mymach.social.auto.reply'
_description = 'Otomatik Yanıt Kuralı'
_order = 'sequence, id'
name = fields.Char(string='Kural Adı', required=True)
active = fields.Boolean(string='Aktif', default=True)
sequence = fields.Integer(string='Öncelik', default=10, help="Düşük sayı daha önce çalışır.")
keyword = fields.Char(string='Tetikleyici Kelime', required=True, help="Mesaj içinde bu kelime geçiyorsa kural çalışır. Küçük/büyük harf duyarlı değildir.")
reply_message = fields.Text(string='Otomatik Yanıt Metni', required=True)
apply_to_message_type = fields.Selection([
('all', 'Tüm Mesaj ve Yorumlarda'),
('comment', 'Sadece Yorumlarda'),
('direct_message', 'Sadece Özel Mesajlarda (DM)')
], string='Mesaj Tipi Filtresi', default='all', required=True)
account_ids = fields.Many2many(
'mymach.social.account',
string='Hedef Hesaplar',
help="Boş bırakılırsa tüm sosyal medya hesaplarında çalışır."
)
log_ids = fields.One2many('mymach.social.auto.reply.log', 'rule_id', string='İşlem Geçmişi')
log_count = fields.Integer(string='Log Sayısı', compute='_compute_log_count')
@api.depends('log_ids')
def _compute_log_count(self):
for rule in self:
rule.log_count = len(rule.log_ids)
def action_view_logs(self):
self.ensure_one()
return {
'name': 'İşlem Geçmişi',
'type': 'ir.actions.act_window',
'res_model': 'mymach.social.auto.reply.log',
'view_mode': 'list,form',
'domain': [('rule_id', '=', self.id)],
'context': {'default_rule_id': self.id},
}

View File

@@ -0,0 +1,22 @@
from odoo import models, fields
class SocialAutoReplyLog(models.Model):
_name = 'mymach.social.auto.reply.log'
_description = 'Otomatik Yanıt İşlem Geçmişi'
_order = 'date desc, id desc'
rule_id = fields.Many2one('mymach.social.auto.reply', string='Tetiklenen Kural', required=True, ondelete='cascade')
post_id = fields.Many2one('mymach.social.stream.post', string='İlgili Mesaj/Yorum', ondelete='set null')
account_id = fields.Many2one('mymach.social.account', string='Sosyal Hesap')
author_name = fields.Char(string='Yazar / Kullanıcı')
trigger_keyword = fields.Char(string='Tetikleyici Kelime')
sent_message = fields.Text(string='Gönderilen Yanıt')
status = fields.Selection([
('success', 'Başarılı'),
('failed', 'Başarısız')
], string='Durum', default='success', required=True)
error_message = fields.Text(string='Hata Mesajı')
date = fields.Datetime(string='İşlem Tarihi', default=fields.Datetime.now, required=True)

View File

@@ -22,5 +22,19 @@ class SocialCompetitor(models.Model):
notes = fields.Text(string='Analysis Notes')
def action_scrape_data(self):
# Placeholder for BeautifulSoup / Scrapy scraping logic
pass
import random
for record in self:
# Mock scraping logic. In a real module, use requests + bs4 or official API
new_followers = record.followers_count + random.randint(10, 500) if record.followers_count else random.randint(5000, 100000)
new_engagement = round(random.uniform(1.5, 8.5), 2)
record.write({
'followers_count': new_followers,
'engagement_rate': new_engagement,
'last_scraped': fields.Datetime.now()
})
@api.model
def _cron_scrape_competitors(self):
competitors = self.search([])
competitors.action_scrape_data()

View File

@@ -0,0 +1,27 @@
from odoo import models, fields, api
class SocialLivePost(models.Model):
_name = 'mymach.social.live.post'
_description = 'Social Live Post'
_rec_name = 'account_id'
post_id = fields.Many2one('mymach.social.post', string='Ana Gönderi', required=True, ondelete='cascade')
account_id = fields.Many2one('mymach.social.account', string='Hesap', required=True, ondelete='cascade')
state = fields.Selection([
('draft', 'Taslak'),
('ready', 'Hazır'),
('posting', 'Gönderiliyor'),
('posted', 'Yayınlandı'),
('failed', 'Hatalı')
], string='Durum', default='draft', required=True)
error_message = fields.Text('Hata Mesajı')
# Platform-specific IDs
platform_post_id = fields.Char(string='Platform Gönderi ID', help="The ID of the post on the social network.")
# Engagement stats for this specific account
likes_count = fields.Integer(string='Beğeniler', default=0)
comments_count = fields.Integer(string='Yorumlar', default=0)
shares_count = fields.Integer(string='Paylaşımlar', default=0)
clicks_count = fields.Integer(string='Tıklamalar', default=0)

View File

@@ -1,27 +1,25 @@
from odoo import models, fields, api
from odoo import models, fields
class SocialMedia(models.Model):
_name = 'social.media'
_description = 'Social Media Library'
_name = 'mymach.social.media'
_description = 'Social Media Asset Library'
_order = 'id desc'
name = fields.Char(string='File Name', required=True)
name = fields.Char(string='Medya Adı', required=True)
media_type = fields.Selection([
('image', 'Image'),
('video', 'Video'),
('template', 'Dynamic Template')
], string='Media Type', required=True)
('image', 'Görsel'),
('video', 'Video (Reels/Shorts)'),
('document', 'Belge/Diğer')
], string='Medya Tipi', default='image', required=True)
file_data = fields.Binary(string='File', required=True)
mimetype = fields.Char(string='Mime Type')
attachment_id = fields.Many2one('ir.attachment', string='Dosya', required=True, ondelete='cascade')
image_preview = fields.Binary(related='attachment_id.datas', string="Önizleme", readonly=True)
# AI/Dynamic generation specific fields
is_ai_generated = fields.Boolean(string='AI Generated', default=False)
template_id = fields.Many2one('social.media', string='Source Template', domain=[('media_type', '=', 'template')])
tag_ids = fields.Many2many('mymach.social.media.tag', string='Etiketler')
def action_generate_watermark(self):
# Placeholder for PIL watermark logic
pass
class SocialMediaTag(models.Model):
_name = 'mymach.social.media.tag'
_description = 'Media Tag'
def action_generate_video(self):
# Placeholder for MoviePy video generation
pass
name = fields.Char(string='Etiket', required=True)
color = fields.Integer(string='Renk İndeksi')

View File

@@ -1,42 +1,297 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
import datetime
import logging
import random
_logger = logging.getLogger(__name__)
class SocialPost(models.Model):
_name = 'social.post'
_name = 'mymach.social.post'
_description = 'Social Media Post'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'scheduled_date desc, id desc'
name = fields.Char(string='Post Title', required=True, tracking=True)
content = fields.Text(string='Content', tracking=True)
name = fields.Char(string='Gönderi Başlığı', required=True, tracking=True)
content = fields.Text(string='İçerik', required=True, tracking=True)
account_ids = fields.Many2many(
'mymach.social.account',
string='Sosyal Medya Hesapları',
required=True
)
post_type = fields.Selection([
('text', 'Sadece Metin'),
('image', 'Görsel (Resim)'),
('video', 'Video (Reels/Shorts/TikTok)')
], string='Gönderi Tipi', default='image', required=True)
media_ids = fields.Many2many('ir.attachment', string='Medyalar (Görseller)')
utm_campaign_id = fields.Many2one('utm.campaign', string='Kampanya (UTM)', help='Odoo pazarlama kampanyası entegrasyonu')
product_id = fields.Many2one('product.template', string='Bağlı Ürün', help='Seçilirse ürün linki ve fiyatı paylaşıma otomatik eklenir.')
scheduled_date = fields.Datetime(string='Planlanan Tarih', tracking=True)
published_date = fields.Datetime(string='Yayınlanma Tarihi', readonly=True, tracking=True)
state = fields.Selection([
('draft', 'Draft'),
('approval', 'Waiting for Approval'),
('scheduled', 'Scheduled'),
('published', 'Published'),
('failed', 'Failed')
], string='Status', default='draft', tracking=True)
('draft', 'Taslak'),
('waiting', 'Onay Bekliyor'),
('scheduled', 'Planlandı'),
('published', 'Yayınlandı'),
('failed', 'Hatalı')
], string='Durum', default='draft', tracking=True)
scheduled_date = fields.Datetime(string='Scheduled Date', tracking=True)
published_date = fields.Datetime(string='Published Date', readonly=True)
error_message = fields.Text(string='Hata Mesajı', readonly=True)
color = fields.Integer(string='Renk İndeksi')
account_ids = fields.Many2many('social.account', string='Social Accounts', required=True)
media_ids = fields.Many2many('social.media', string='Media Files')
# Analytics Metrics
likes_count = fields.Integer(string='Beğeniler', readonly=True, default=0, tracking=True)
comments_count = fields.Integer(string='Yorumlar', readonly=True, default=0, tracking=True)
shares_count = fields.Integer(string='Paylaşımlar', readonly=True, default=0, tracking=True)
error_message = fields.Text(string='Error Message', readonly=True)
click_count = fields.Integer(string='Tıklamalar / Ziyaretçiler', compute='_compute_click_count')
def action_submit_for_approval(self):
for record in self:
record.state = 'approval'
def _compute_click_count(self):
for post in self:
if not post.utm_campaign_id:
post.click_count = 0
continue
# Gelişmiş link.tracker sorgusu - bu kampanya üzerinden gelen tıklamalar
trackers = self.env['link.tracker'].search([('campaign_id', '=', post.utm_campaign_id.id)])
post.click_count = sum(trackers.mapped('count'))
def action_approve(self):
for record in self:
if record.scheduled_date:
record.state = 'scheduled'
else:
record.action_publish()
live_post_ids = fields.One2many('mymach.social.live.post', 'post_id', string='Canlı Gönderiler', readonly=True)
# 1. Gönderi Önizleme (Live Preview)
preview_html = fields.Html(string="Önizleme", compute="_compute_preview_html")
# 5. Platform Validasyonu (Constraint)
@api.constrains('content', 'account_ids')
def _check_content_length(self):
for post in self:
if not post.content:
continue
for account in post.account_ids:
if account.platform == 'twitter' and len(post.content) > 280:
raise UserError(f"X (Twitter) için karakter sınırı 280'dir. Şu an {len(post.content)} karakter yazdınız.")
elif account.platform == 'linkedin' and len(post.content) > 3000:
raise UserError(f"LinkedIn için karakter sınırı 3000'dir. Şu an {len(post.content)} karakter yazdınız.")
@api.depends('content', 'account_ids', 'media_ids', 'product_id', 'post_type')
def _compute_preview_html(self):
for post in self:
if not post.content and not post.media_ids:
post.preview_html = "<div class='text-muted text-center p-5'>Gönderi önizlemesi için içerik veya medya ekleyin...</div>"
continue
# Mobil Mock-up tasarımı (iPhone Tarzı)
preview_content = post.content or ""
if post.product_id:
price = post.product_id.list_price
link = f"https://mymach.com/shop/product/{post.product_id.id}"
preview_content += f"\n\nSatın Al: {post.product_id.name} - {price}\n{link}"
media_label = "Medyalar Yüklenecek"
if post.post_type == 'video':
media_label = f"▶ Video Önizlemesi ({len(post.media_ids)} Dosya)"
elif post.post_type == 'image':
media_label = f"📷 Görsel Alanı ({len(post.media_ids)} Dosya)"
preview = f"""
<div style="margin: 0 auto; width: 320px; height: 600px; border: 12px solid #333; border-radius: 36px; position: relative; background: #fafafa; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.1);">
<!-- Notch -->
<div style="position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 120px; height: 25px; background: #333; border-bottom-left-radius: 15px; border-bottom-right-radius: 15px; z-index: 10;"></div>
<!-- App Header -->
<div style="padding: 35px 15px 10px; background: #fff; border-bottom: 1px solid #ddd; display: flex; align-items: center; justify-content: space-between;">
<div style="font-weight: bold; font-family: sans-serif; font-size: 18px;">SocialApp</div>
</div>
<!-- Post Area -->
<div style="background: #fff; margin-top: 10px; padding: 15px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
<!-- User Info -->
<div style="display: flex; align-items: center; margin-bottom: 12px;">
<div style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%); display: flex; align-items: center; justify-content: center; margin-right: 10px;">
<div style="width: 32px; height: 32px; border-radius: 50%; background: #fff; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; color: #333;">MM</div>
</div>
<div>
<div style="font-weight: 600; font-size: 13px; color: #262626;">MyMach Official</div>
<div style="color: #8e8e8e; font-size: 11px;">Sponsored</div>
</div>
</div>
<!-- Media Placeholder -->
<div style="width: 100%; height: 250px; background: #efefef; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #8e8e8e; font-size: 14px; margin-bottom: 12px;">
{media_label}
</div>
<!-- Actions -->
<div style="display: flex; gap: 12px; margin-bottom: 10px; color: #262626; font-size: 18px;">
♡ 💬 ➦
</div>
<!-- Content -->
<div style="font-size: 13px; color: #262626; white-space: pre-wrap; line-height: 1.4;">
<span style="font-weight: 600;">MyMach Official</span> {preview_content}
</div>
</div>
</div>
"""
post.preview_html = preview
# 4. Link Takibi ve UTM (Auto-Link Tracker)
def _extract_and_shorten_links(self):
import re
for post in self:
if not post.content:
continue
# Temel URL regex'i
urls = re.findall(r'(https?://[^\s]+)', post.content)
new_content = post.content
for url in urls:
# Odoo'nun link.tracker modelini kullanarak UTM'li kısa link yarat
tracker = self.env['link.tracker'].create({
'url': url,
'campaign_id': post.utm_campaign_id.id if post.utm_campaign_id else False,
'title': f"{post.name} Linki"
})
# Kısa linkle orijinali değiştir
new_content = new_content.replace(url, tracker.short_url)
if new_content != post.content:
post.content = new_content
def action_view_visitors(self):
self.ensure_one()
return {
'name': 'Bağlantı İzleyici (Ziyaretçiler)',
'type': 'ir.actions.act_window',
'res_model': 'link.tracker',
'view_mode': 'list,form',
'domain': [('campaign_id', '=', self.utm_campaign_id.id)]
}
def action_open_ai_wizard(self):
self.ensure_one()
return {
'name': 'AI ile İçerik Üret',
'type': 'ir.actions.act_window',
'res_model': 'mymach.social.ai.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_post_id': self.id}
}
def action_open_ai_image_wizard(self):
self.ensure_one()
return {
'name': 'AI ile Görsel Üret',
'type': 'ir.actions.act_window',
'res_model': 'mymach.social.ai.image.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_post_id': self.id}
}
def action_request_approval(self):
self.write({'state': 'waiting'})
def action_schedule(self):
for post in self:
if not post.scheduled_date:
raise UserError("Lütfen planlamak için bir tarih belirleyin.")
if post.scheduled_date < fields.Datetime.now():
raise UserError("Planlanmış tarih geçmişte olamaz.")
self.write({'state': 'scheduled'})
def action_publish_now(self):
self.write({'scheduled_date': fields.Datetime.now()})
self.action_publish()
def action_publish(self):
for record in self:
# Placeholder for publishing logic
record.state = 'published'
record.published_date = fields.Datetime.now()
# Eğer ürün seçiliyse metne ekle
for post in self:
if post.product_id and post.content:
price = post.product_id.list_price
# UTM Etiketleri Ekle (Analytics İzleme)
campaign_name = post.utm_campaign_id.name if post.utm_campaign_id else "social_post"
campaign_name = campaign_name.replace(' ', '_').lower()
link = f"https://mymach.com/shop/product/{post.product_id.id}?utm_source=mymach_social&utm_medium=social&utm_campaign={campaign_name}"
append_text = f"\n\nSatın Al: {post.product_id.name} - {price}\n{link}"
if append_text not in post.content:
post.content += append_text
# Yayınlamadan önce linkleri kısalt ve UTM ekle
self._extract_and_shorten_links()
for post in self:
try:
# Create a live post for each selected account
for account in post.account_ids:
# Check if it already exists to prevent duplicates on retry
existing_live = self.env['mymach.social.live.post'].search([
('post_id', '=', post.id),
('account_id', '=', account.id)
], limit=1)
if not existing_live:
live_post = self.env['mymach.social.live.post'].create({
'post_id': post.id,
'account_id': account.id,
'state': 'ready'
})
api_client = None
try:
api_client = account._get_api_client()
except Exception as e:
_logger.warning(f"Hesap API client alınamadı: {e}")
platform_post_id = False
if account.platform == 'facebook' and account.access_token and api_client:
page_id = account.platform_account_id or 'me'
# API İsteği
resp = api_client.publish_post(page_id, account.access_token, post.content)
platform_post_id = resp.get('id', False)
else:
# Diğer platformlar mock (şimdilik)
platform_post_id = f"PLATFORM_{account.id}_{post.id}"
live_post.write({
'state': 'posted',
'likes_count': 0,
'comments_count': 0,
'platform_post_id': platform_post_id or f"PLATFORM_{account.id}_{post.id}"
})
# Overall post metrics can be aggregated or simulated
post.write({
'state': 'published',
'published_date': fields.Datetime.now(),
'error_message': False,
'likes_count': sum(post.live_post_ids.mapped('likes_count')),
'comments_count': sum(post.live_post_ids.mapped('comments_count')),
'shares_count': sum(post.live_post_ids.mapped('shares_count')),
})
except Exception as e:
post.write({
'state': 'failed',
'error_message': f"Yayınlama Hatası: {str(e)}"
})
def action_reset_to_draft(self):
self.write({'state': 'draft', 'error_message': False})
@api.model
def _cron_publish_scheduled_posts(self):
_logger.info("Cron: Zamanlanmış gönderiler kontrol ediliyor...")
posts_to_publish = self.search([
('state', '=', 'scheduled'),
('scheduled_date', '<=', fields.Datetime.now())
])
if posts_to_publish:
_logger.info(f"Cron: Yayınlanacak {len(posts_to_publish)} gönderi bulundu.")
posts_to_publish.action_publish()

View File

@@ -0,0 +1,17 @@
from odoo import models, fields, api
class SocialStream(models.Model):
_name = 'mymach.social.stream'
_description = 'Social Media Stream'
name = fields.Char(string='Akış Adı', required=True)
account_id = fields.Many2one('mymach.social.account', string='Hesap', required=True, ondelete='cascade')
stream_post_ids = fields.One2many('mymach.social.stream.post', 'stream_id', string='Akış Gönderileri')
stream_type = fields.Selection([
('feed', 'Zaman Tüneli / Feed'),
('mentions', 'Bahsetmeler'),
('messages', 'Mesajlar'),
('keyword', 'Anahtar Kelime (Dinleme)')
], string='Akış Tipi', default='feed', required=True)
search_keyword = fields.Char(string='Takip Edilecek Kelime/Hashtag', help='Sadece Akış Tipi "Anahtar Kelime" ise kullanılır. Örn: #teknoloji')

View File

@@ -0,0 +1,140 @@
from odoo import models, fields, api
class SocialStreamPost(models.Model):
_name = 'mymach.social.stream.post'
_inherit = ['mail.thread']
_description = 'Social Stream Post'
_order = 'published_date desc'
stream_id = fields.Many2one('mymach.social.stream', string='Akış', required=True, ondelete='cascade')
account_id = fields.Many2one(related='stream_id.account_id', string='Hesap', store=True)
author_name = fields.Char(string='Yazar')
author_link = fields.Char(string='Yazar Profil Linki')
published_date = fields.Datetime(string='Yayın Tarihi')
message = fields.Text(string='Mesaj / İçerik')
link_url = fields.Char(string='Gönderi Linki')
likes_count = fields.Integer(string='Beğeniler')
comments_count = fields.Integer(string='Yorumlar')
shares_count = fields.Integer(string='Paylaşımlar')
platform_post_id = fields.Char(string='Platform ID')
sentiment = fields.Selection([
('positive', '😊 Olumlu'),
('neutral', '😐 Nötr'),
('negative', '😡 Olumsuz')
], string='Duygu Analizi', default='neutral')
message_type = fields.Selection([
('comment', 'Yorum'),
('direct_message', 'Özel Mesaj (DM)')
], string='Mesaj Tipi', default='comment', required=True)
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for record in records:
if not record.message:
continue
# Otomatik Yanıt Kurallarını Kontrol Et
rules = self.env['mymach.social.auto.reply'].search([('active', '=', True)], order='sequence, id')
for rule in rules:
# Hesap Filtresi Kontrolü
if rule.account_ids and record.account_id not in rule.account_ids:
continue
# Mesaj Tipi Filtresi Kontrolü
if rule.apply_to_message_type != 'all' and rule.apply_to_message_type != record.message_type:
continue
# Kelime Kontrolü
if rule.keyword.lower() in record.message.lower():
# Yanıtı gönder
record.message_post(body=f"🤖 <b>Otomatik Yanıt Gönderildi:</b><br/>'{rule.keyword}' kelimesi algılandı.<br/><b>Yanıt:</b> {rule.reply_message}")
# Log kaydını oluştur
self.env['mymach.social.auto.reply.log'].create({
'rule_id': rule.id,
'post_id': record.id,
'account_id': record.account_id.id,
'author_name': record.author_name,
'trigger_keyword': rule.keyword,
'sent_message': rule.reply_message,
'status': 'success'
})
break # İlk eşleşen kural çalışsın ve döngü bitsin.
return records
def action_analyze_sentiment(self):
# API anahtarını al
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
for post in self:
if not post.message:
continue
if api_key:
# Gerçek AI Analizi (Gemini)
try:
import requests
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
system_prompt = "Sen bir duygu analizi uzmanısın. Verilen metnin duygusunu analiz et ve sadece şu üç kelimeden birini döndür: 'positive', 'neutral', 'negative'."
payload = {
"system_instruction": {"parts": [{"text": system_prompt}]},
"contents": [{"parts": [{"text": f"Metin: {post.message}"}]}],
"generationConfig": {"temperature": 0.1, "maxOutputTokens": 10}
}
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload)
if response.status_code == 200:
result = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '').strip().lower()
if 'positive' in result:
post.sentiment = 'positive'
elif 'negative' in result:
post.sentiment = 'negative'
else:
post.sentiment = 'neutral'
continue
except Exception as e:
# Log error, fallback to basic logic
pass
# Fallback (API Key yoksa veya hata olursa) Basit kelime analizi
lower_msg = post.message.lower()
if any(word in lower_msg for word in ['harika', 'süper', 'teşekkür', 'iyi', 'başarılı']):
post.sentiment = 'positive'
elif any(word in lower_msg for word in ['kötü', 'rezalet', 'şikayet', 'sorun', 'hata', 'berbat']):
post.sentiment = 'negative'
else:
post.sentiment = 'neutral'
def action_suggest_reply(self):
self.ensure_one()
return {
'name': 'AI ile Yanıt Öner',
'type': 'ir.actions.act_window',
'res_model': 'mymach.social.ai.reply.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_stream_post_id': self.id}
}
def action_create_lead(self):
self.ensure_one()
# Odoo CRM'de yeni bir Lead (Fırsat/Müşteri Adayı) yarat
lead = self.env['crm.lead'].create({
'name': f"Sosyal Medya Fırsatı: {self.author_name}",
'description': f"İçerik: {self.message}\nLink: {self.link_url}",
'type': 'opportunity'
})
return {
'name': 'Yeni Müşteri Adayı',
'type': 'ir.actions.act_window',
'res_model': 'crm.lead',
'res_id': lead.id,
'view_mode': 'form',
'target': 'current'
}

View File

@@ -1,9 +1,14 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_social_account_user,social.account.user,model_social_account,base.group_user,1,1,1,0
access_social_account_manager,social.account.manager,model_social_account,base.group_system,1,1,1,1
access_social_post_user,social.post.user,model_social_post,base.group_user,1,1,1,0
access_social_post_manager,social.post.manager,model_social_post,base.group_system,1,1,1,1
access_social_media_user,social.media.user,model_social_media,base.group_user,1,1,1,0
access_social_media_manager,social.media.manager,model_social_media,base.group_system,1,1,1,1
access_social_competitor_user,social.competitor.user,model_social_competitor,base.group_user,1,1,1,0
access_social_competitor_manager,social.competitor.manager,model_social_competitor,base.group_system,1,1,1,1
access_mymach_social_account_user,mymach.social.account.user,model_mymach_social_account,base.group_user,1,1,1,1
access_mymach_social_post_user,mymach.social.post.user,model_mymach_social_post,base.group_user,1,1,1,1
access_mymach_social_ai_wizard_user,mymach.social.ai.wizard.user,model_mymach_social_ai_wizard,base.group_user,1,1,1,1
access_mymach_social_live_post_user,mymach.social.live.post.user,model_mymach_social_live_post,base.group_user,1,1,1,1
access_mymach_social_stream_user,mymach.social.stream.user,model_mymach_social_stream,base.group_user,1,1,1,1
access_mymach_social_stream_post_user,mymach.social.stream.post.user,model_mymach_social_stream_post,base.group_user,1,1,1,1
access_mymach_social_competitor_user,social.competitor.user,model_social_competitor,base.group_user,1,1,1,1
access_mymach_social_ai_image_wizard_user,mymach.social.ai.image.wizard.user,model_mymach_social_ai_image_wizard,base.group_user,1,1,1,1
access_mymach_social_ai_reply_wizard_user,mymach.social.ai.reply.wizard.user,model_mymach_social_ai_reply_wizard,base.group_user,1,1,1,1
access_social_media,access.mymach.social.media,model_mymach_social_media,base.group_user,1,1,1,1
access_social_media_tag,access.mymach.social.media.tag,model_mymach_social_media_tag,base.group_user,1,1,1,1
access_social_auto_reply,access.mymach.social.auto.reply,model_mymach_social_auto_reply,base.group_user,1,1,1,1
access_social_auto_reply_log,access.mymach.social.auto.reply.log,model_mymach_social_auto_reply_log,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_social_account_user access_mymach_social_account_user social.account.user mymach.social.account.user model_social_account model_mymach_social_account base.group_user 1 1 1 0 1
3 access_social_account_manager access_mymach_social_post_user social.account.manager mymach.social.post.user model_social_account model_mymach_social_post base.group_system base.group_user 1 1 1 1
4 access_social_post_user access_mymach_social_ai_wizard_user social.post.user mymach.social.ai.wizard.user model_social_post model_mymach_social_ai_wizard base.group_user 1 1 1 0 1
5 access_social_post_manager access_mymach_social_live_post_user social.post.manager mymach.social.live.post.user model_social_post model_mymach_social_live_post base.group_system base.group_user 1 1 1 1
6 access_social_media_user access_mymach_social_stream_user social.media.user mymach.social.stream.user model_social_media model_mymach_social_stream base.group_user 1 1 1 0 1
7 access_social_media_manager access_mymach_social_stream_post_user social.media.manager mymach.social.stream.post.user model_social_media model_mymach_social_stream_post base.group_system base.group_user 1 1 1 1
8 access_social_competitor_user access_mymach_social_competitor_user social.competitor.user model_social_competitor base.group_user 1 1 1 0 1
9 access_social_competitor_manager access_mymach_social_ai_image_wizard_user social.competitor.manager mymach.social.ai.image.wizard.user model_social_competitor model_mymach_social_ai_image_wizard base.group_system base.group_user 1 1 1 1
10 access_mymach_social_ai_reply_wizard_user mymach.social.ai.reply.wizard.user model_mymach_social_ai_reply_wizard base.group_user 1 1 1 1
11 access_social_media access.mymach.social.media model_mymach_social_media base.group_user 1 1 1 1
12 access_social_media_tag access.mymach.social.media.tag model_mymach_social_media_tag base.group_user 1 1 1 1
13 access_social_auto_reply access.mymach.social.auto.reply model_mymach_social_auto_reply base.group_user 1 1 1 1
14 access_social_auto_reply_log access.mymach.social.auto.reply.log model_mymach_social_auto_reply_log base.group_user 1 1 1 1

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!--
Account Modeli İçin Kurallar
-->
<record id="rule_social_account_user" model="ir.rule">
<field name="name">Social Account: User Rule</field>
<field name="model_id" ref="model_mymach_social_account"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<record id="rule_social_account_manager" model="ir.rule">
<field name="name">Social Account: Manager Rule</field>
<field name="model_id" ref="model_mymach_social_account"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<!--
Post Modeli İçin Kurallar
-->
<record id="rule_social_post_user" model="ir.rule">
<field name="name">Social Post: User Rule</field>
<field name="model_id" ref="model_mymach_social_post"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<record id="rule_social_post_manager" model="ir.rule">
<field name="name">Social Post: Manager Rule</field>
<field name="model_id" ref="model_mymach_social_post"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Sosyal Medya Uzmanı (Normal Kullanıcı) -->
<record id="group_social_user" model="res.groups">
<field name="name">Sosyal Medya Uzmanı</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="comment">Gönderi taslağı hazırlayabilir ve onay bekler statüsüne alabilir.</field>
</record>
<!-- Sosyal Medya Yöneticisi (Manager) -->
<record id="group_social_manager" model="res.groups">
<field name="name">Sosyal Medya Yöneticisi</field>
<field name="implied_ids" eval="[(4, ref('group_social_user'))]"/>
<field name="comment">Taslakları onaylayabilir, doğrudan yayınlayabilir ve gelişmiş ayarları görebilir.</field>
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,137 @@
/* mymach_social_nextgen/static/src/css/social_dashboard.css */
/* 1. KANBAN VIEW (ANA ARKA PLAN) */
.o_kanban_view.o_social_dashboard_kanban {
background-color: #020617 !important; /* Çok daha koyu bir arka plan (Slate-950) */
padding: 16px !important;
min-height: 100vh !important;
}
/* 2. SÜTUNLAR (STREAMS) */
.o_social_dashboard_kanban .o_kanban_group {
background-color: transparent !important;
border: none !important;
padding: 0 8px !important;
}
/* Sütun Başlığı (Header) */
.o_social_dashboard_kanban .o_kanban_header {
background-color: #334155 !important; /* Arka plana göre çok daha açık bir renk (Slate-700) */
border: 1px solid #475569 !important;
border-radius: 12px 12px 0 0 !important;
padding: 16px !important;
border-bottom: none !important;
display: flex;
align-items: center;
}
/* Sütun Başlığı Metni */
.o_social_dashboard_kanban .o_kanban_header .o_column_title,
.o_social_dashboard_kanban .o_kanban_header span,
.o_social_dashboard_kanban .o_kanban_header div {
color: #ffffff !important;
font-weight: 700 !important;
font-size: 16px !important;
letter-spacing: 0.3px;
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
}
/* Sütun İçeriği (Kartların dizildiği yer) */
.o_social_dashboard_kanban .o_kanban_record_has_image_fill,
.o_social_dashboard_kanban .o_kanban_group > .o_kanban_record {
background: transparent !important;
}
/* 3. KARTLAR (POSTS) */
.o_social_dashboard_kanban .o_kanban_record {
background-color: #1e293b !important; /* Başlıkla aynı uyumlu renk */
border: 1px solid #334155 !important;
border-radius: 12px !important;
margin-top: 12px !important;
margin-bottom: 12px !important;
color: #e2e8f0 !important;
padding: 16px !important;
transition: all 0.2s ease-in-out;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
display: flex;
flex-direction: column;
}
/* Kart Hover Efekti */
.o_social_dashboard_kanban .o_kanban_record:hover {
border-color: #64748b !important;
transform: translateY(-3px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1) !important;
}
/* Yazar Bilgisi Alanı */
.o_social_author_section {
display: flex;
align-items: center;
margin-bottom: 16px;
}
/* Yazar Avatarı */
.o_social_author_avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6, #8b5cf6); /* Şık bir gradient */
margin-right: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.o_social_author_name {
font-weight: 600;
color: #f1f5f9;
font-size: 14px;
}
.o_social_time {
color: #94a3b8;
font-size: 12px;
margin-top: 2px;
}
/* Post İçeriği (Mesaj) */
.o_social_message_text {
font-size: 14px;
line-height: 1.6;
color: #cbd5e1;
margin-bottom: 20px;
white-space: pre-wrap;
word-break: break-word;
flex-grow: 1; /* Mesaj kısmının esnek olması için */
}
/* Alt Aksiyon Butonları (Beğeni, Yorum, Paylaş) */
.o_social_actions {
display: flex;
gap: 20px;
color: #94a3b8;
font-size: 14px;
border-top: 1px solid #334155;
padding-top: 14px;
margin-top: auto;
}
.o_social_actions div {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: color 0.2s;
}
.o_social_actions div:hover {
color: #38bdf8; /* Hover durumunda tatlı bir mavi */
}
.o_social_actions i {
font-size: 16px;
}
/* Odoo'nun Default Ekleme/Yükleme Butonlarını Gizle (Gerekirse) */
.o_social_dashboard_kanban .o_kanban_group .o_kanban_header .o_kanban_config {
color: #94a3b8 !important;
}

View File

@@ -0,0 +1,2 @@
from . import test_social_core
from . import test_social_auto_reply

View File

@@ -0,0 +1,85 @@
from odoo.tests.common import TransactionCase
class TestSocialAutoReply(TransactionCase):
@classmethod
def setUpClass(cls):
super(TestSocialAutoReply, cls).setUpClass()
# Test izolasyonu için mevcut tüm kuralları pasif yap
cls.env['mymach.social.auto.reply'].search([]).write({'active': False})
# Testler için gerekli verileri oluştur
cls.social_account = cls.env['mymach.social.account'].create({
'name': 'Test Account',
'platform': 'facebook',
'active': True,
})
cls.stream = cls.env['mymach.social.stream'].create({
'name': 'Test Akışı',
'account_id': cls.social_account.id,
'stream_type': 'feed'
})
def test_01_auto_reply_rule_creation(self):
"""Otomatik yanıt kuralı oluşturulabildiğini test et."""
rule = self.env['mymach.social.auto.reply'].create({
'name': 'Fiyat Kuralı',
'keyword': 'fiyat',
'reply_message': 'Fiyatlarımız için sitemize bakınız.',
'active': True
})
self.assertEqual(rule.keyword, 'fiyat')
self.assertEqual(rule.log_count, 0)
def test_02_auto_reply_trigger_and_log(self):
"""Bir gönderi oluşturulduğunda kuralın tetiklenip log oluşturduğunu test et."""
# 1. Kuralı oluştur
rule = self.env['mymach.social.auto.reply'].create({
'name': 'Fiyat Kuralı',
'keyword': 'fiyat',
'reply_message': 'Fiyatlarımız web sitemizdedir.',
'active': True
})
# Başlangıçta log yok
self.assertEqual(rule.log_count, 0)
# 2. 'fiyat' kelimesini içeren bir Stream Post oluştur
# Oluşturma işlemi (create) sırasında `mymach.social.stream.post` kuralı kontrol edip log atacak
post = self.env['mymach.social.stream.post'].create({
'stream_id': self.stream.id,
'author_name': 'Müşteri',
'message': 'Merhaba, ürünün fiyat listesini alabilir miyim?',
'message_type': 'comment'
})
# 3. Logun oluştuğunu doğrula
log = self.env['mymach.social.auto.reply.log'].search([('rule_id', '=', rule.id)])
self.assertEqual(len(log), 1)
rule.invalidate_recordset()
self.assertEqual(rule.log_count, 1)
self.assertEqual(log.status, 'success')
self.assertEqual(log.post_id.id, post.id)
self.assertEqual(log.trigger_keyword, 'fiyat')
def test_03_no_auto_reply_if_no_keyword(self):
"""Kural kelimesi eşleşmediğinde log oluşmadığını test et."""
rule = self.env['mymach.social.auto.reply'].create({
'name': 'Şikayet Kuralı',
'keyword': 'şikayet',
'reply_message': 'Lütfen destek hattını arayın.',
'active': True
})
# 'fiyat' geçen ama 'şikayet' geçmeyen mesaj
self.env['mymach.social.stream.post'].create({
'stream_id': self.stream.id,
'author_name': 'Müşteri 2',
'message': 'Sadece fiyat sormuştum.',
'message_type': 'comment'
})
self.assertEqual(rule.log_count, 0)

View File

@@ -0,0 +1,43 @@
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError
class TestSocialCore(TransactionCase):
@classmethod
def setUpClass(cls):
super(TestSocialCore, cls).setUpClass()
# Test hesabı oluştur
cls.social_account = cls.env['mymach.social.account'].create({
'name': 'Test Account',
'platform': 'facebook',
'active': True,
})
def test_01_social_account_creation(self):
"""Sosyal hesabın doğru ilişkilerle oluşturulduğunu test et."""
self.assertEqual(self.social_account.platform, 'facebook')
self.assertTrue(self.social_account.active)
def test_02_social_post_creation(self):
"""Sosyal gönderi oluşturma ve durum (state) doğrulama."""
post = self.env['mymach.social.post'].create({
'name': 'Test Gönderisi',
'content': 'Bu bir test gönderisidir.',
'account_ids': [(4, self.social_account.id)]
})
self.assertEqual(post.state, 'draft')
self.assertIn(self.social_account, post.account_ids)
# Post the message
post.action_publish()
self.assertEqual(post.state, 'published')
def test_03_social_stream_creation(self):
"""Sosyal akış (stream) oluşturmayı test et."""
stream = self.env['mymach.social.stream'].create({
'name': 'Test Akışı',
'account_id': self.social_account.id,
'stream_type': 'feed'
})
self.assertEqual(stream.name, 'Test Akışı')
self.assertEqual(stream.account_id.name, 'Test Account')

View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.social</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="90"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app data-string="MyMach Social" string="MyMach Social" name="mymach_social_nextgen" groups="base.group_system">
<block title="Sosyal Medya API Kimlik Bilgileri" name="social_api_credentials_setting_container">
<setting string="Meta (Facebook/Instagram)" help="Meta Geliştirici Uygulaması için Client ID ve Secret">
<div class="content-group">
<div class="row mt16">
<label for="social_meta_client_id" class="col-lg-3 o_light_label"/>
<field name="social_meta_client_id" class="oe_inline"/>
</div>
<div class="row mt8">
<label for="social_meta_client_secret" class="col-lg-3 o_light_label"/>
<field name="social_meta_client_secret" class="oe_inline" password="True"/>
</div>
</div>
</setting>
<setting string="LinkedIn" help="LinkedIn Geliştirici Uygulaması için Client ID ve Secret">
<div class="content-group">
<div class="row mt16">
<label for="social_linkedin_client_id" class="col-lg-3 o_light_label"/>
<field name="social_linkedin_client_id" class="oe_inline"/>
</div>
<div class="row mt8">
<label for="social_linkedin_client_secret" class="col-lg-3 o_light_label"/>
<field name="social_linkedin_client_secret" class="oe_inline" password="True"/>
</div>
</div>
</setting>
<setting string="X (Twitter)" help="X Geliştirici Portalı için Client ID ve Secret">
<div class="content-group">
<div class="row mt16">
<label for="social_x_client_id" class="col-lg-3 o_light_label"/>
<field name="social_x_client_id" class="oe_inline"/>
</div>
<div class="row mt8">
<label for="social_x_client_secret" class="col-lg-3 o_light_label"/>
<field name="social_x_client_secret" class="oe_inline" password="True"/>
</div>
</div>
</setting>
<setting string="YouTube" help="Google Cloud Console için Client ID ve Secret">
<div class="content-group">
<div class="row mt16">
<label for="social_youtube_client_id" class="col-lg-3 o_light_label"/>
<field name="social_youtube_client_id" class="oe_inline"/>
</div>
<div class="row mt8">
<label for="social_youtube_client_secret" class="col-lg-3 o_light_label"/>
<field name="social_youtube_client_secret" class="oe_inline" password="True"/>
</div>
</div>
</setting>
</block>
<block title="Yapay Zeka Asistan Ayarları" name="social_ai_settings_container">
<setting string="Google Gemini" help="Gemini Pro Entegrasyonu için API Anahtarı">
<div class="content-group">
<div class="row mt16">
<label for="social_gemini_api_key" class="col-lg-3 o_light_label"/>
<field name="social_gemini_api_key" class="oe_inline" password="True"/>
</div>
</div>
</setting>
<setting string="OpenAI (ChatGPT)" help="OpenAI modelleri için API Anahtarı">
<div class="content-group">
<div class="row mt16">
<label for="social_openai_api_key" class="col-lg-3 o_light_label"/>
<field name="social_openai_api_key" class="oe_inline" password="True"/>
</div>
</div>
</setting>
<setting string="Anthropic (Claude)" help="Claude AI modelleri için API Anahtarı">
<div class="content-group">
<div class="row mt16">
<label for="social_claude_api_key" class="col-lg-3 o_light_label"/>
<field name="social_claude_api_key" class="oe_inline" password="True"/>
</div>
</div>
</setting>
<setting string="xAI (Grok)" help="Grok modelleri için API Anahtarı">
<div class="content-group">
<div class="row mt16">
<label for="social_grok_api_key" class="col-lg-3 o_light_label"/>
<field name="social_grok_api_key" class="oe_inline" password="True"/>
</div>
</div>
</setting>
</block>
<block title="Medya Kütüphanesi &amp; Filigran (Watermark) Ayarları" name="social_watermark_settings_container">
<setting string="Şirket Logosu (Filigran)" help="Üretilen veya yüklenen medyaların üzerine otomatik basılacak PNG formatında şeffaf logo.">
<div class="content-group">
<div class="row mt16">
<label for="social_watermark_image" class="col-lg-3 o_light_label"/>
<field name="social_watermark_image" widget="image" class="oe_avatar"/>
</div>
<div class="row mt8">
<label for="social_watermark_position" class="col-lg-3 o_light_label"/>
<field name="social_watermark_position" class="oe_inline"/>
</div>
</div>
</setting>
</block>
</app>
</xpath>
</field>
</record>
<!-- Action for Settings -->
<record id="action_social_config_settings" model="ir.actions.act_window">
<field name="name">Ayarlar</field>
<field name="res_model">res.config.settings</field>
<field name="view_id" ref="res_config_settings_view_form"/>
<field name="view_mode">form</field>
<field name="context">{'module' : 'mymach_social_nextgen', 'bin_size': False}</field>
</record>
<menuitem id="menu_social_settings"
name="Ayarlar"
parent="menu_social_configuration"
action="action_social_config_settings"
sequence="100"/>
</data>
</odoo>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Tree View -->
<record id="view_social_account_tree" model="ir.ui.view">
<field name="name">mymach.social.account.list</field>
<field name="model">mymach.social.account</field>
<field name="arch" type="xml">
<list string="Social Accounts">
<field name="name"/>
<field name="platform"/>
<field name="active"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_social_account_form" model="ir.ui.view">
<field name="name">mymach.social.account.form</field>
<field name="model">mymach.social.account</field>
<field name="arch" type="xml">
<form string="Sosyal Medya Hesabı">
<header>
<button name="action_authenticate" string="Platforma Bağlan" type="object" class="oe_highlight" invisible="access_token != False"/>
<button name="action_refresh_token" string="Token Yenile" type="object" invisible="access_token == False"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_authenticate" type="object" class="oe_stat_button" icon="fa-users">
<field name="audience_count" widget="statinfo" string="Takipçiler"/>
</button>
<button name="action_authenticate" type="object" class="oe_stat_button" icon="fa-line-chart">
<field name="engagement_rate" widget="statinfo" string="Etkileşim %"/>
</button>
</div>
<div class="oe_title">
<h1><field name="name" placeholder="Hesap Adı..."/></h1>
</div>
<group>
<group>
<field name="platform"/>
<field name="active"/>
</group>
<group>
<field name="platform_account_id" invisible="platform_account_id == False"/>
<field name="token_expiration" invisible="access_token == False"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_social_account" model="ir.actions.act_window">
<field name="name">Sosyal Hesaplar</field>
<field name="res_model">mymach.social.account</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first Social Media Account connection!
</p>
</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Graph View -->
<record id="view_social_post_graph" model="ir.ui.view">
<field name="name">mymach.social.post.graph</field>
<field name="model">mymach.social.post</field>
<field name="arch" type="xml">
<graph string="Sosyal Medya Analizleri" type="bar" sample="1">
<field name="scheduled_date" interval="month"/>
<field name="state"/>
<field name="likes_count" type="measure"/>
</graph>
</field>
</record>
<!-- Pivot View for Analytics -->
<record id="view_social_post_pivot" model="ir.ui.view">
<field name="name">mymach.social.post.pivot</field>
<field name="model">mymach.social.post</field>
<field name="arch" type="xml">
<pivot string="Analiz Tablosu" sample="1">
<field name="scheduled_date" type="row" interval="month"/>
<field name="state" type="col"/>
<field name="likes_count" type="measure"/>
<field name="comments_count" type="measure"/>
<field name="shares_count" type="measure"/>
</pivot>
</field>
</record>
<!-- Sentiment Analysis Pie Chart -->
<record id="view_social_sentiment_graph" model="ir.ui.view">
<field name="name">mymach.social.sentiment.graph</field>
<field name="model">mymach.social.stream.post</field>
<field name="arch" type="xml">
<graph string="Duygu Analizi" type="pie" sample="1">
<field name="sentiment"/>
</graph>
</field>
</record>
<record id="action_social_sentiment_dashboard" model="ir.actions.act_window">
<field name="name">Duygu Analizi Panosu</field>
<field name="res_model">mymach.social.stream.post</field>
<field name="view_mode">graph,list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Müşteri Geri Bildirimleri Analizi
</p>
</field>
</record>
<!-- Dashboard Action -->
<record id="action_social_dashboard" model="ir.actions.act_window">
<field name="name">Analiz Dashboard</field>
<field name="res_model">mymach.social.post</field>
<field name="view_mode">graph,pivot,kanban,list</field>
<field name="context">{'search_default_state': 'published'}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Sosyal Medya Performansınızı Takip Edin!
</p>
<p>
Gönderilerinizi yayınladıkça etkileşim grafikleriniz burada belirecektir.
</p>
</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Log List (Tree) View -->
<record id="view_social_auto_reply_log_tree" model="ir.ui.view">
<field name="name">mymach.social.auto.reply.log.tree</field>
<field name="model">mymach.social.auto.reply.log</field>
<field name="arch" type="xml">
<list string="Otomatik Yanıt Geçmişi" decoration-success="status == 'success'" decoration-danger="status == 'failed'">
<field name="date" widget="datetime"/>
<field name="rule_id"/>
<field name="author_name"/>
<field name="account_id"/>
<field name="trigger_keyword"/>
<field name="status" widget="badge" decoration-success="status == 'success'" decoration-danger="status == 'failed'"/>
</list>
</field>
</record>
<!-- Log Form View -->
<record id="view_social_auto_reply_log_form" model="ir.ui.view">
<field name="name">mymach.social.auto.reply.log.form</field>
<field name="model">mymach.social.auto.reply.log</field>
<field name="arch" type="xml">
<form string="Otomatik Yanıt İşlemi">
<header>
<field name="status" widget="statusbar" statusbar_visible="success,failed"/>
</header>
<sheet>
<group>
<group>
<field name="rule_id" readonly="1"/>
<field name="date" readonly="1"/>
<field name="account_id" readonly="1"/>
</group>
<group>
<field name="author_name" readonly="1"/>
<field name="trigger_keyword" readonly="1"/>
<field name="post_id" readonly="1"/>
</group>
</group>
<notebook>
<page string="Gönderilen Yanıt">
<field name="sent_message" readonly="1"/>
</page>
<page string="Hata Mesajı" invisible="status == 'success'">
<field name="error_message" readonly="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_social_auto_reply_log_search" model="ir.ui.view">
<field name="name">mymach.social.auto.reply.log.search</field>
<field name="model">mymach.social.auto.reply.log</field>
<field name="arch" type="xml">
<search>
<field name="rule_id"/>
<field name="author_name"/>
<field name="account_id"/>
<separator/>
<filter string="Başarılı" name="success" domain="[('status', '=', 'success')]"/>
<filter string="Başarısız" name="failed" domain="[('status', '=', 'failed')]"/>
<separator/>
<filter string="Kurala Göre Grupla" name="group_rule" context="{'group_by':'rule_id'}"/>
<filter string="Tarihe Göre Grupla" name="group_date" context="{'group_by':'date:day'}"/>
</search>
</field>
</record>
<!-- Action -->
<record id="action_social_auto_reply_log" model="ir.actions.act_window">
<field name="name">İşlem Geçmişi</field>
<field name="res_model">mymach.social.auto.reply.log</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_social_auto_reply_log_search"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Form View -->
<record id="view_social_auto_reply_form" model="ir.ui.view">
<field name="name">mymach.social.auto.reply.form</field>
<field name="model">mymach.social.auto.reply</field>
<field name="arch" type="xml">
<form string="Otomatik Yanıt Kuralı">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_logs" type="object" class="oe_stat_button" icon="fa-history" invisible="log_count == 0">
<field name="log_count" widget="statinfo" string="İşlemler"/>
</button>
</div>
<widget name="web_ribbon" title="Pasif" bg_color="bg-danger" invisible="active == True"/>
<div class="oe_title">
<h1>
<field name="name" placeholder="Kural Adı (Örn: Bilgi Talebi)"/>
</h1>
</div>
<group>
<group string="Kural Ayarları">
<field name="keyword" placeholder="bilgi"/>
<field name="active" widget="boolean_toggle"/>
<field name="sequence"/>
</group>
<group string="Filtreler">
<field name="apply_to_message_type" widget="radio" options="{'horizontal': true}"/>
<field name="account_ids" widget="many2many_tags" placeholder="Tüm hesaplar"/>
</group>
</group>
<group string="Yanıt İçeriği">
<field name="reply_message" nolabel="1" widget="text" placeholder="Gelecek olan mesajda bu kelime geçiyorsa otomatik olarak şu metni gönder..."/>
</group>
</sheet>
</form>
</field>
</record>
<!-- List View -->
<record id="view_social_auto_reply_list" model="ir.ui.view">
<field name="name">mymach.social.auto.reply.list</field>
<field name="model">mymach.social.auto.reply</field>
<field name="arch" type="xml">
<list string="Otomatik Yanıt Kuralları">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="keyword"/>
<field name="active" widget="boolean_toggle"/>
</list>
</field>
</record>
<!-- Action -->
<record id="action_social_auto_reply" model="ir.actions.act_window">
<field name="name">Otomatik Yanıt Kuralları</field>
<field name="res_model">mymach.social.auto.reply</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
İlk Otomatik Yanıt Kuralınızı Oluşturun!
</p>
<p>
Gelen yorum veya mesajlarda spesifik anahtar kelimeler ("bilgi", "fiyat" vb.) geçtiğinde sistemin otomatik yanıt vermesini sağlayabilirsiniz.
</p>
</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- List View -->
<record id="view_social_competitor_list" model="ir.ui.view">
<field name="name">social.competitor.list</field>
<field name="model">social.competitor</field>
<field name="arch" type="xml">
<list string="Rakipler">
<field name="name"/>
<field name="platform"/>
<field name="followers_count"/>
<field name="engagement_rate"/>
<field name="last_scraped"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_social_competitor_form" model="ir.ui.view">
<field name="name">social.competitor.form</field>
<field name="model">social.competitor</field>
<field name="arch" type="xml">
<form string="Rakip Bilgisi">
<header>
<button name="action_scrape_data" string="Verileri Kazı (Güncelle)" type="object" class="oe_highlight"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Rakip Firma Adı"/>
</h1>
</div>
<group>
<group>
<field name="platform"/>
<field name="profile_url" widget="url"/>
</group>
<group>
<field name="followers_count"/>
<field name="engagement_rate"/>
<field name="last_scraped"/>
</group>
</group>
<notebook>
<page string="Analiz Notları">
<field name="notes" placeholder="Stratejik notlar..."/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Kanban View -->
<record id="view_social_competitor_kanban" model="ir.ui.view">
<field name="name">social.competitor.kanban</field>
<field name="model">social.competitor</field>
<field name="arch" type="xml">
<kanban class="o_kanban_dashboard">
<field name="name"/>
<field name="platform"/>
<field name="followers_count"/>
<field name="engagement_rate"/>
<template>
<t t-name="card">
<div class="oe_kanban_global_click">
<div class="o_kanban_record_title">
<strong><field name="name"/></strong>
</div>
<div class="mt-2">
<span class="badge rounded-pill text-bg-primary"><field name="platform"/></span>
</div>
<div class="mt-2">
<i class="fa fa-users" title="Takipçiler"/> <field name="followers_count"/>
</div>
<div class="mt-1">
<i class="fa fa-line-chart" title="Etkileşim Oranı"/> <field name="engagement_rate"/> %
</div>
</div>
</t>
</template>
</kanban>
</field>
</record>
<!-- Graph View -->
<record id="view_social_competitor_graph" model="ir.ui.view">
<field name="name">social.competitor.graph</field>
<field name="model">social.competitor</field>
<field name="arch" type="xml">
<graph string="Rakip Takipçi Karşılaştırması" type="bar">
<field name="name"/>
<field name="followers_count" type="measure"/>
</graph>
</field>
</record>
<!-- Action -->
<record id="action_social_competitor" model="ir.actions.act_window">
<field name="name">Rakipler</field>
<field name="res_model">social.competitor</field>
<field name="view_mode">kanban,list,graph,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
İlk Rakibinizi Ekleyin!
</p>
<p>
Rakiplerinizin sosyal medya hesaplarını ekleyerek durumlarını izleyin.
</p>
</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_social_competitors"
name="Rakipler"
parent="mymach_social_nextgen.menu_social_root"
action="action_social_competitor"
sequence="35"/>
</data>
</odoo>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_social_live_post_list" model="ir.ui.view">
<field name="name">mymach.social.live.post.list</field>
<field name="model">mymach.social.live.post</field>
<field name="arch" type="xml">
<list string="Canlı Gönderiler" decoration-info="state == 'ready'" decoration-success="state == 'posted'" decoration-danger="state == 'failed'">
<field name="account_id"/>
<field name="post_id"/>
<field name="state" widget="badge"/>
<field name="likes_count"/>
<field name="comments_count"/>
<field name="shares_count"/>
</list>
</field>
</record>
<!-- Action -->
<record id="action_social_live_post" model="ir.actions.act_window">
<field name="name">Canlı Gönderiler</field>
<field name="res_model">mymach.social.live.post</field>
<field name="view_mode">list,form</field>
<field name="domain">[('post_id', '=', active_id)]</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Media Form View -->
<record id="view_social_media_form" model="ir.ui.view">
<field name="name">mymach.social.media.form</field>
<field name="model">mymach.social.media</field>
<field name="arch" type="xml">
<form string="Medya Kütüphanesi">
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Medya Adı..."/>
</h1>
</div>
<group>
<group>
<field name="media_type"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
<group>
<field name="attachment_id"/>
<field name="image_preview" widget="image" class="oe_avatar" invisible="media_type != 'image'"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Media List View -->
<record id="view_social_media_list" model="ir.ui.view">
<field name="name">mymach.social.media.list</field>
<field name="model">mymach.social.media</field>
<field name="arch" type="xml">
<list string="Medya Kütüphanesi">
<field name="name"/>
<field name="media_type"/>
<field name="tag_ids" widget="many2many_tags"/>
</list>
</field>
</record>
<!-- Media Kanban View -->
<record id="view_social_media_kanban" model="ir.ui.view">
<field name="name">mymach.social.media.kanban</field>
<field name="model">mymach.social.media</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile">
<field name="name"/>
<field name="media_type"/>
<field name="image_preview"/>
<field name="tag_ids"/>
<template>
<t t-name="card">
<div class="oe_kanban_global_click">
<div class="o_kanban_image">
<field name="image_preview" widget="image" options="{'size': [90, 90]}"/>
</div>
<div class="oe_kanban_details">
<strong><field name="name"/></strong>
<div><field name="media_type"/></div>
<div><field name="tag_ids" widget="many2many_tags"/></div>
</div>
</div>
</t>
</template>
</kanban>
</field>
</record>
<!-- Media Action -->
<record id="action_social_media" model="ir.actions.act_window">
<field name="name">Medya Kütüphanesi</field>
<field name="res_model">mymach.social.media</field>
<field name="view_mode">kanban,list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Medya Kütüphanenize ilk dosyanızı ekleyin!
</p>
</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Top Level Menu -->
<menuitem id="menu_social_root"
name="MyMach Social"
web_icon="mymach_social_nextgen,static/description/icon.png"
sequence="50"/>
<!-- Dashboard (Default Home) -->
<menuitem id="menu_social_dashboard"
name="Kontrol Paneli"
parent="menu_social_root"
action="action_social_dashboard_kanban"
sequence="10"/>
<!-- Calendar Action -->
<record id="action_social_calendar" model="ir.actions.act_window">
<field name="name">Takvim</field>
<field name="res_model">mymach.social.post</field>
<field name="view_mode">calendar,kanban,list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Aylık Gönderi Planınız Burada Görünecek!
</p>
</field>
</record>
<!-- Sentiment Dashboard -->
<menuitem id="menu_social_sentiment"
name="Duygu Analizi"
parent="menu_social_root"
action="action_social_sentiment_dashboard"
sequence="12"/>
<!-- Inbox / Live Chat -->
<menuitem id="menu_social_inbox"
name="Gelen Kutusu (Sohbet)"
parent="menu_social_root"
action="action_social_inbox"
sequence="15"/>
<!-- Calendar Menu -->
<menuitem id="menu_social_calendar"
name="Takvim"
parent="menu_social_root"
action="action_social_calendar"
sequence="18"/>
<!-- Posts Menu -->
<menuitem id="menu_social_posts"
name="Gönderiler"
parent="menu_social_root"
action="action_social_post"
sequence="20"/>
<!-- Streams Menu -->
<menuitem id="menu_social_streams"
name="Akışlar (Streams)"
parent="menu_social_root"
action="action_social_stream"
sequence="25"/>
<!-- Campaigns Action -->
<record id="action_social_campaign" model="ir.actions.act_window">
<field name="name">Kampanyalar</field>
<field name="res_model">utm.campaign</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Pazarlama Kampanyalarınızı Buradan Yönetin
</p>
</field>
</record>
<!-- Campaigns Menu -->
<menuitem id="menu_social_campaigns"
name="Kampanyalar"
parent="menu_social_root"
action="action_social_campaign"
sequence="30"/>
<!-- Media Library Menu -->
<menuitem id="menu_social_media_library"
name="Medya Kütüphanesi"
parent="menu_social_root"
action="action_social_media"
sequence="35"/>
<!-- Visitors Action -->
<record id="action_social_visitor" model="ir.actions.act_window">
<field name="name">Ziyaretçiler (Tıklamalar)</field>
<field name="res_model">link.tracker.click</field>
<field name="view_mode">list,form</field>
<field name="context">{'create': False, 'edit': False}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Sosyal Medya Ziyaretçilerinizi (Tıklamaları) Buradan Takip Edin
</p>
</field>
</record>
<!-- Visitors Menu -->
<menuitem id="menu_social_visitors"
name="Ziyaretçiler"
parent="menu_social_root"
action="action_social_visitor"
sequence="40"/>
<!-- Configuration Menu -->
<menuitem id="menu_social_configuration"
name="Yapılandırma"
parent="menu_social_root"
sequence="100"/>
<!-- Accounts Menu (Under Configuration) -->
<menuitem id="menu_social_accounts_config"
name="Sosyal Hesaplar"
parent="menu_social_configuration"
action="action_social_account"
sequence="10"/>
<!-- Auto-Reply Rules Menu (Under Configuration) -->
<menuitem id="menu_social_auto_reply_config"
name="Otomatik Yanıt Kuralları"
parent="menu_social_configuration"
action="action_social_auto_reply"
sequence="20"/>
<!-- Auto-Reply Logs Menu (Under Configuration) -->
<menuitem id="menu_social_auto_reply_logs_config"
name="Otomatik Yanıt Geçmişi"
parent="menu_social_configuration"
action="action_social_auto_reply_log"
sequence="30"/>
</data>
</odoo>

View File

@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Form View -->
<record id="view_social_post_form" model="ir.ui.view">
<field name="name">mymach.social.post.form</field>
<field name="model">mymach.social.post</field>
<field name="arch" type="xml">
<form string="Sosyal Medya Gönderisi">
<header>
<button name="action_open_ai_wizard" string="✨ AI ile İçerik Üret" type="object" class="btn-info" invisible="state != 'draft'"/>
<button name="action_open_ai_image_wizard" string="🎨 AI Görsel Üret" type="object" class="btn-primary" invisible="state != 'draft'"/>
<button name="action_request_approval" string="Onaya Gönder" type="object" invisible="state != 'draft'"/>
<button name="action_schedule" string="Planla" type="object" class="oe_highlight" invisible="state not in ('draft', 'waiting')" groups="mymach_social_nextgen.group_social_manager"/>
<button name="action_publish_now" string="Onayla ve Yayınla" type="object" class="btn-warning" invisible="state not in ('draft', 'waiting')" groups="mymach_social_nextgen.group_social_manager"/>
<button name="action_reset_to_draft" string="Taslağa Çevir" type="object" invisible="state == 'draft'" groups="mymach_social_nextgen.group_social_manager"/>
<field name="state" widget="statusbar" statusbar_visible="draft,waiting,scheduled,published"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="%(action_social_live_post)d" type="action" class="oe_stat_button" icon="fa-share-square-o" invisible="state not in ('published', 'failed')">
<field name="live_post_ids" widget="statinfo" string="Canlı Gönderiler"/>
</button>
<button name="action_view_visitors" type="object" class="oe_stat_button" icon="fa-mouse-pointer" invisible="state not in ('published', 'failed')">
<field name="click_count" widget="statinfo" string="Ziyaretçiler"/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" placeholder="Gönderi Başlığı..."/>
</h1>
</div>
<group>
<group>
<field name="account_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="scheduled_date" invisible="state == 'published'"/>
<field name="published_date" invisible="state != 'published'"/>
</group>
<group>
<field name="media_ids" widget="many2many_binary" string="Görseller (Ekler)"/>
<field name="post_type" widget="radio" options="{'horizontal': true}"/>
<field name="utm_campaign_id"/>
<field name="product_id" string="E-Ticaret Ürünü" help="Seçilen ürünün satın alma linki ve fiyatı paylaşıma otomatik eklenecek."/>
</group>
</group>
<notebook>
<page string="İçerik">
<group>
<group>
<field name="content" widget="text" placeholder="Gönderinizi buraya yazın..." nolabel="1" colspan="2"/>
</group>
<group>
<field name="preview_html" widget="html" nolabel="1" colspan="2" readonly="1"/>
</group>
</group>
</page>
<page string="Hata Mesajı" invisible="state != 'failed'">
<field name="error_message"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- List View -->
<record id="view_social_post_list" model="ir.ui.view">
<field name="name">mymach.social.post.list</field>
<field name="model">mymach.social.post</field>
<field name="arch" type="xml">
<list string="Sosyal Medya Gönderileri" decoration-info="state == 'scheduled'" decoration-success="state == 'published'" decoration-danger="state == 'failed'" decoration-warning="state == 'waiting'">
<field name="name"/>
<field name="account_ids" widget="many2many_tags"/>
<field name="scheduled_date"/>
<field name="published_date"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<!-- Kanban View -->
<record id="view_social_post_kanban" model="ir.ui.view">
<field name="name">mymach.social.post.kanban</field>
<field name="model">mymach.social.post</field>
<field name="arch" type="xml">
<kanban default_group_by="state" class="o_kanban_small_column" quick_create="false">
<field name="name"/>
<field name="state"/>
<field name="color"/>
<field name="scheduled_date"/>
<field name="account_ids"/>
<template>
<t t-name="card">
<div class="oe_kanban_global_click">
<div class="oe_kanban_content">
<div class="o_kanban_record_title">
<strong><field name="name"/></strong>
</div>
<div class="o_kanban_record_bottom mt-2">
<div class="oe_kanban_bottom_left">
<field name="account_ids" widget="many2many_tags"/>
</div>
<div class="oe_kanban_bottom_right">
<field name="scheduled_date"/>
</div>
</div>
</div>
</div>
</t>
</template>
</kanban>
</field>
</record>
<!-- Calendar View -->
<record id="view_social_post_calendar" model="ir.ui.view">
<field name="name">mymach.social.post.calendar</field>
<field name="model">mymach.social.post</field>
<field name="arch" type="xml">
<calendar string="Sosyal Medya Gönderileri" date_start="scheduled_date" color="state" mode="month" quick_create="0">
<field name="name"/>
<field name="account_ids"/>
<field name="state" filters="1"/>
</calendar>
</field>
</record>
<!-- Action -->
<record id="action_social_post" model="ir.actions.act_window">
<field name="name">Gönderiler</field>
<field name="res_model">mymach.social.post</field>
<field name="view_mode">kanban,list,calendar,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
İlk Sosyal Medya Gönderinizi Oluşturun!
</p>
<p>
İçeriklerinizi hazırlayın, medyalarınızı ekleyin ve planlayın.
</p>
</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Social Dashboard Kanban View -->
<record id="view_social_stream_post_kanban_dashboard" model="ir.ui.view">
<field name="name">mymach.social.stream.post.kanban.dashboard</field>
<field name="model">mymach.social.stream.post</field>
<field name="arch" type="xml">
<kanban class="o_social_dashboard_kanban" default_group_by="stream_id" create="0">
<field name="author_name"/>
<field name="published_date"/>
<field name="message"/>
<field name="likes_count"/>
<field name="comments_count"/>
<field name="shares_count"/>
<field name="stream_id"/>
<templates>
<t t-name="card">
<div t-attf-class="oe_kanban_global_click o_kanban_record">
<!-- Author & Time Info -->
<div class="o_social_author_section">
<div class="o_social_author_avatar d-flex align-items-center justify-content-center text-white">
<i class="fa fa-user"/>
</div>
<div class="d-flex flex-column">
<span class="o_social_author_name"><field name="author_name"/></span>
<span class="o_social_time"><field name="published_date"/></span>
</div>
</div>
<!-- Post Content -->
<div class="o_social_message_text">
<field name="message"/>
</div>
<!-- Actions / Metrics -->
<div class="o_social_actions">
<div title="Beğeniler">
<i class="fa fa-heart-o me-1"/> <field name="likes_count"/>
</div>
<div title="Yorumlar">
<i class="fa fa-comment-o me-1"/> <field name="comments_count"/>
</div>
<div title="Paylaşımlar">
<i class="fa fa-share-square-o me-1"/> <field name="shares_count"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- Dashboard Action -->
<record id="action_social_dashboard_kanban" model="ir.actions.act_window">
<field name="name">Kontrol Paneli</field>
<field name="res_model">mymach.social.stream.post</field>
<field name="view_mode">kanban,form</field>
<field name="view_id" ref="view_social_stream_post_kanban_dashboard"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Harika Sosyal Medya Kontrol Panelinize Hoşgeldiniz!
</p>
<p>
Burada yapılandırdığınız akışlar, ekranınızda sütunlar halinde listelenecektir.
</p>
</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Stream Kanban View -->
<record id="view_social_stream_kanban" model="ir.ui.view">
<field name="name">mymach.social.stream.kanban</field>
<field name="model">mymach.social.stream</field>
<field name="arch" type="xml">
<kanban class="o_kanban_dashboard" create="1">
<field name="name"/>
<field name="account_id"/>
<field name="stream_type"/>
<field name="search_keyword"/>
<template>
<t t-name="card">
<div class="oe_kanban_global_click">
<div class="o_kanban_record_title">
<strong><field name="name"/></strong>
</div>
<div><field name="account_id"/></div>
<div><field name="stream_type"/></div>
<div t-if="record.search_keyword.raw_value" class="text-info">
<i class="fa fa-search" title="Aranan Kelime"/> <field name="search_keyword"/>
</div>
</div>
</t>
</template>
</kanban>
</field>
</record>
<!-- Stream Form View -->
<record id="view_social_stream_form" model="ir.ui.view">
<field name="name">mymach.social.stream.form</field>
<field name="model">mymach.social.stream</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="account_id"/>
<field name="stream_type"/>
<field name="search_keyword" invisible="stream_type != 'keyword'" required="stream_type == 'keyword'"/>
</group>
<notebook>
<page string="Gönderiler ve Yorumlar">
<field name="stream_post_ids">
<list string="Akış Gönderileri">
<field name="author_name"/>
<field name="message"/>
<field name="likes_count"/>
<field name="comments_count"/>
<button name="action_create_lead" type="object" string="CRM Fırsatı Yarat" icon="fa-star" class="btn-success"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_social_stream" model="ir.actions.act_window">
<field name="name">Akışlar (Streams)</field>
<field name="res_model">mymach.social.stream</field>
<field name="view_mode">kanban,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
İlk Sosyal Medya Akışınızı Ekleyin!
</p>
<p>
Sosyal medya hesaplarınıza gelen yorum ve mesajları buradan canlı takip edebilirsiniz.
</p>
</field>
</record>
<!-- Live Chat (Inbox) Action -->
<record id="action_social_inbox" model="ir.actions.act_window">
<field name="name">Canlı Sohbet (Gelen Kutusu)</field>
<field name="res_model">mymach.social.stream.post</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Sosyal Medya Mesajlarınız Burada Toplanacak!
</p>
</field>
</record>
<!-- Live Chat List View -->
<record id="view_social_stream_post_list" model="ir.ui.view">
<field name="name">mymach.social.stream.post.list</field>
<field name="model">mymach.social.stream.post</field>
<field name="arch" type="xml">
<list string="Gelen Mesajlar" decoration-info="True" create="0">
<field name="published_date"/>
<field name="author_name" string="Gönderen"/>
<field name="account_id" string="Hesap"/>
<field name="message_type" widget="badge" decoration-info="message_type == 'direct_message'" decoration-muted="message_type == 'comment'"/>
<field name="message" string="Mesaj İçeriği"/>
<field name="sentiment" widget="badge" decoration-success="sentiment == 'positive'" decoration-danger="sentiment == 'negative'" decoration-warning="sentiment == 'neutral'"/>
<button name="action_analyze_sentiment" type="object" string="Duyguyu Analiz Et" icon="fa-smile-o"/>
<button name="action_create_lead" type="object" string="Fırsat Yarat" icon="fa-star" class="btn-success"/>
</list>
</field>
</record>
<!-- Live Chat Form View -->
<record id="view_social_stream_post_form" model="ir.ui.view">
<field name="name">mymach.social.stream.post.form</field>
<field name="model">mymach.social.stream.post</field>
<field name="arch" type="xml">
<form string="Mesaj Detayı" create="0">
<header>
<button name="action_suggest_reply" type="object" string="✨ AI ile Yanıt Öner" class="btn-info"/>
<button name="action_analyze_sentiment" type="object" string="Duyguyu Analiz Et"/>
<button name="action_create_lead" type="object" string="CRM Fırsatı Yarat" class="oe_highlight"/>
</header>
<sheet>
<group>
<group>
<field name="author_name" readonly="1"/>
<field name="account_id" readonly="1"/>
<field name="published_date" readonly="1"/>
<field name="message_type" widget="badge" decoration-info="message_type == 'direct_message'" decoration-muted="message_type == 'comment'"/>
<field name="sentiment" widget="badge" decoration-success="sentiment == 'positive'" decoration-danger="sentiment == 'negative'" decoration-warning="sentiment == 'neutral'"/>
</group>
<group>
<field name="likes_count" readonly="1"/>
<field name="comments_count" readonly="1"/>
<field name="shares_count" readonly="1"/>
</group>
</group>
<notebook>
<page string="Mesaj İçeriği">
<field name="message" widget="text" readonly="1"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,3 @@
from . import social_ai_wizard
from . import social_ai_image_wizard
from . import social_ai_reply_wizard

View File

@@ -0,0 +1,43 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class SocialAIImageWizard(models.TransientModel):
_name = 'mymach.social.ai.image.wizard'
_description = 'AI Image Generator Wizard'
post_id = fields.Many2one('mymach.social.post', string='Post', required=True)
image_prompt = fields.Text(string='Görsel Tarifi', required=True,
default='Örn: Masada duran şık bir kahve fincanı, arka planda gün batımı.')
def action_generate_image(self):
self.ensure_one()
try:
import requests
import urllib.parse
import base64
encoded_prompt = urllib.parse.quote(self.image_prompt)
# Pollinations AI generates images for free without API keys.
pollinations_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true"
img_data = requests.get(pollinations_url, timeout=30).content
encoded_image = base64.b64encode(img_data)
media_record = self.env['ir.attachment'].create({
'name': f"AI_Generated_Image.png",
'datas': encoded_image,
'res_model': 'mymach.social.post',
'res_id': self.post_id.id,
'type': 'binary',
})
self.post_id.write({'media_ids': [(4, media_record.id)]})
except Exception as e:
_logger.error(f"Image Generation Error: {str(e)}")
raise UserError(f"Görsel üretilirken hata oluştu: {str(e)}")

View File

@@ -0,0 +1,141 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class SocialAIReplyWizard(models.TransientModel):
_name = 'mymach.social.ai.reply.wizard'
_description = 'AI Reply Generator Wizard'
stream_post_id = fields.Many2one('mymach.social.stream.post', string='Mesaj', required=True)
customer_message = fields.Text(related='stream_post_id.message', string="Müşteri Mesajı")
suggested_reply = fields.Text(string='Üretilen Yanıt')
ai_provider = fields.Selection([
('gemini', 'Google Gemini'),
('openai', 'OpenAI (ChatGPT)'),
('claude', 'Anthropic (Claude)'),
('grok', 'xAI (Grok)')
], string='Yapay Zeka Sağlayıcısı', default='gemini', required=True)
def action_generate_reply(self):
self.ensure_one()
system_instruction = "Sen profesyonel bir müşteri temsilcisisin. Gelen sosyal medya mesajına uygun, kibar, çözüm odaklı ve kurumsal bir yanıt önerisi oluştur. Kısa ve net ol."
user_prompt = f"Müşteri Mesajı: {self.customer_message}\n\nLütfen bu mesaja profesyonel bir yanıt üret."
try:
import requests
import json
generated_text = ""
if self.ai_provider == 'gemini':
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
if not api_key: raise UserError("Google Gemini API Anahtarı eksik!")
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
payload = {
"system_instruction": {"parts": [{"text": system_instruction}]},
"contents": [{"parts": [{"text": user_prompt}]}],
"generationConfig": {"temperature": 0.5, "maxOutputTokens": 200}
}
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload)
if response.status_code != 200: raise UserError(f"Gemini API Hatası: {response.text}")
generated_text = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '')
elif self.ai_provider == 'openai':
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key')
if not api_key: raise UserError("OpenAI API Anahtarı eksik!")
url = "https://api.openai.com/v1/chat/completions"
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
payload = {
"model": "gpt-4o",
"messages": [
{"role": "system", "content": system_instruction},
{"role": "user", "content": user_prompt}
],
"temperature": 0.5,
"max_tokens": 200
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200: raise UserError(f"OpenAI API Hatası: {response.text}")
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
elif self.ai_provider == 'claude':
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.claude_api_key')
if not api_key: raise UserError("Claude API Anahtarı eksik!")
url = "https://api.anthropic.com/v1/messages"
headers = {
'x-api-key': api_key,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
}
payload = {
"model": "claude-3-5-sonnet-20240620",
"system": system_instruction,
"messages": [{"role": "user", "content": user_prompt}],
"max_tokens": 200,
"temperature": 0.5
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200: raise UserError(f"Claude API Hatası: {response.text}")
generated_text = response.json().get('content', [{}])[0].get('text', '')
elif self.ai_provider == 'grok':
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.grok_api_key')
if not api_key: raise UserError("Grok API Anahtarı eksik!")
url = "https://api.x.ai/v1/chat/completions"
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
payload = {
"model": "grok-2",
"messages": [
{"role": "system", "content": system_instruction},
{"role": "user", "content": user_prompt}
],
"temperature": 0.5,
"max_tokens": 200
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200: raise UserError(f"Grok API Hatası: {response.text}")
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
if not generated_text:
raise UserError(f"{self.ai_provider} boş bir yanıt döndürdü.")
self.suggested_reply = generated_text.strip()
return {
'type': 'ir.actions.act_window',
'res_model': 'mymach.social.ai.reply.wizard',
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
except Exception as e:
_logger.error(f"AI API Error: {str(e)}")
raise UserError(f"AI ile iletişim kurulamadı: {str(e)}")
def action_send_reply(self):
self.ensure_one()
if not self.suggested_reply:
raise UserError("Gönderilecek bir yanıt yok.")
# In a real app, this would call the Meta Graph API to post the reply to the comment/message.
# For our NextGen simulation, we will just log it or simulate it.
# In this demo, let's just close the wizard and show a success notification.
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Başarılı',
'message': 'Mesaj başarıyla müşteriye iletildi!',
'type': 'success',
'sticky': False,
}
}

View File

@@ -0,0 +1,168 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class SocialAIWizard(models.TransientModel):
_name = 'mymach.social.ai.wizard'
_description = 'AI Content Generator Wizard'
post_id = fields.Many2one('mymach.social.post', string='Post', required=True)
prompt = fields.Text(string='Gönderi Konusu', required=True,
default='Örn: Yeni çıkardığımız CNC makinesinin hız ve hassasiyet özelliklerini anlatan bir tanıtım gönderisi.')
tone = fields.Selection([
('professional', 'Profesyonel'),
('casual', 'Günlük ve Samimi'),
('humorous', 'Eğlenceli / Esprili'),
('formal', 'Resmi'),
], string='Ses Tonu', default='professional', required=True)
include_hashtags = fields.Boolean(string='Trend Hashtag\'ler Eklensin', default=True)
# Free Image Generation Fields
generate_image = fields.Boolean(string='Görsel Üret (Yapay Zeka - Ücretsiz)', default=False)
image_prompt = fields.Text(string='Görsel Tarifi', help='Eğer boş bırakılırsa, Gönderi Konusu kullanılacaktır.')
ai_provider = fields.Selection([
('gemini', 'Google Gemini'),
('openai', 'OpenAI (ChatGPT)'),
('claude', 'Anthropic (Claude)'),
('grok', 'xAI (Grok)'),
('none', 'Hiçbiri (Sadece Görsel Üret veya Manuel)')
], string='Yapay Zeka Sağlayıcısı', default='gemini', required=True)
def action_generate_content(self):
self.ensure_one()
# --- 1. Handle Text Generation if not 'none' ---
generated_text = ""
if self.ai_provider != 'none':
tone_map = dict(self._fields['tone'].selection).get(self.tone)
system_instruction = f"Sen uzman bir sosyal medya yöneticisisin. Verilen konuya göre Türkçe, etkileyici ve ilgi çekici bir sosyal medya gönderisi yaz. Tonlama: {tone_map}."
if self.include_hashtags:
system_instruction += " Metnin sonuna konuyla ilgili 3-5 adet trend hashtag ekle."
user_prompt = f"Konu: {self.prompt}\n\nLütfen bu konuya göre sosyal medya içeriğini üret."
try:
import requests
import json
if self.ai_provider == 'gemini':
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
if not api_key: raise UserError("Google Gemini API Anahtarı eksik!")
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
payload = {
"system_instruction": {"parts": [{"text": system_instruction}]},
"contents": [{"parts": [{"text": user_prompt}]}],
"generationConfig": {"temperature": 0.7, "maxOutputTokens": 400}
}
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload)
if response.status_code != 200: raise UserError(f"Gemini API Hatası: {response.text}")
generated_text = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '')
elif self.ai_provider == 'openai':
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key')
if not api_key: raise UserError("OpenAI API Anahtarı eksik!")
url = "https://api.openai.com/v1/chat/completions"
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
payload = {
"model": "gpt-4o",
"messages": [
{"role": "system", "content": system_instruction},
{"role": "user", "content": user_prompt}
],
"temperature": 0.7,
"max_tokens": 400
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200: raise UserError(f"OpenAI API Hatası: {response.text}")
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
elif self.ai_provider == 'claude':
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.claude_api_key')
if not api_key: raise UserError("Claude API Anahtarı eksik!")
url = "https://api.anthropic.com/v1/messages"
headers = {
'x-api-key': api_key,
'anthropic-version': '2023-06-01',
'content-type': 'application/json'
}
payload = {
"model": "claude-3-5-sonnet-20240620",
"system": system_instruction,
"messages": [{"role": "user", "content": user_prompt}],
"max_tokens": 400,
"temperature": 0.7
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200: raise UserError(f"Claude API Hatası: {response.text}")
generated_text = response.json().get('content', [{}])[0].get('text', '')
elif self.ai_provider == 'grok':
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.grok_api_key')
if not api_key: raise UserError("Grok API Anahtarı eksik!")
url = "https://api.x.ai/v1/chat/completions"
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
payload = {
"model": "grok-2",
"messages": [
{"role": "system", "content": system_instruction},
{"role": "user", "content": user_prompt}
],
"temperature": 0.7,
"max_tokens": 400
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200: raise UserError(f"Grok API Hatası: {response.text}")
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
if not generated_text:
raise UserError(f"{self.ai_provider} boş bir yanıt döndürdü.")
generated_text = generated_text.strip()
except Exception as e:
_logger.error(f"AI API Error: {str(e)}")
raise UserError(f"AI ile iletişim kurulamadı: {str(e)}")
# --- 2. Generate Image (Free Stable Diffusion via Pollinations) ---
media_record = False
if self.generate_image:
import urllib.parse
import base64
import requests
img_prompt = self.image_prompt if self.image_prompt else self.prompt
encoded_prompt = urllib.parse.quote(img_prompt)
pollinations_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true"
try:
img_data = requests.get(pollinations_url, timeout=30).content
encoded_image = base64.b64encode(img_data)
media_record = self.env['ir.attachment'].create({
'name': f"AI_Generated_Image.png",
'datas': encoded_image,
'res_model': 'mymach.social.post',
'res_id': self.post_id.id,
'type': 'binary',
})
except Exception as img_e:
_logger.error(f"Stable Diffusion Image Error: {str(img_e)}")
raise UserError(f"Ücretsiz görsel üretilirken hata oluştu: {str(img_e)}")
# --- 3. Update the post ---
update_vals = {}
if generated_text:
update_vals['content'] = generated_text
if media_record:
update_vals['media_ids'] = [(4, media_record.id)]
if update_vals:
self.post_id.write(update_vals)

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_social_ai_wizard_form" model="ir.ui.view">
<field name="name">mymach.social.ai.wizard.form</field>
<field name="model">mymach.social.ai.wizard</field>
<field name="arch" type="xml">
<form string="Yapay Zeka İçerik Üreticisi">
<group>
<field name="post_id" invisible="1"/>
<field name="ai_provider"/>
<field name="prompt" invisible="ai_provider == 'none'"/>
<field name="tone" invisible="ai_provider == 'none'"/>
<field name="include_hashtags" invisible="ai_provider == 'none'"/>
</group>
<group string="Görsel Üretimi (Ücretsiz Yapay Zeka)">
<field name="generate_image" widget="boolean_toggle"/>
<field name="image_prompt" invisible="generate_image == False"/>
<div class="alert alert-info" role="alert" invisible="not generate_image">
Görseller <b>Ücretsiz (Stable Diffusion)</b> altyapısı ile üretilecektir. Herhangi bir API anahtarı veya ek bakiye gerekmez!
</div>
</group>
<footer>
<button name="action_generate_content" string="Üret (Generate)" type="object" class="btn-primary"/>
<button string="İptal" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_social_ai_wizard" model="ir.actions.act_window">
<field name="name">AI ile İçerik Üret</field>
<field name="res_model">mymach.social.ai.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- AI Image Wizard -->
<record id="view_social_ai_image_wizard_form" model="ir.ui.view">
<field name="name">mymach.social.ai.image.wizard.form</field>
<field name="model">mymach.social.ai.image.wizard</field>
<field name="arch" type="xml">
<form string="Yapay Zeka Görsel Üreticisi">
<group>
<field name="post_id" invisible="1"/>
<field name="image_prompt"/>
</group>
<div class="alert alert-info" role="alert">
Görseller <b>Ücretsiz (Stable Diffusion)</b> altyapısı ile üretilecektir. Herhangi bir API anahtarı veya ek bakiye gerekmez!
</div>
<footer>
<button name="action_generate_image" string="Görsel Üret" type="object" class="btn-primary"/>
<button string="İptal" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- AI Reply Wizard -->
<record id="view_social_ai_reply_wizard_form" model="ir.ui.view">
<field name="name">mymach.social.ai.reply.wizard.form</field>
<field name="model">mymach.social.ai.reply.wizard</field>
<field name="arch" type="xml">
<form string="AI Yanıt Asistanı">
<group>
<field name="stream_post_id" invisible="1"/>
<field name="ai_provider"/>
<field name="customer_message" readonly="1"/>
<field name="suggested_reply"/>
</group>
<footer>
<button name="action_generate_reply" string="AI Yanıt Üret" type="object" class="btn-info" invisible="suggested_reply != False"/>
<button name="action_send_reply" string="Yanıtı Gönder" type="object" class="btn-success" invisible="not suggested_reply"/>
<button string="Kapat" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

24
standalone-social-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}

View File

@@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the Oxlint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project.

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>standalone-social-app</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

1396
standalone-social-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
{
"name": "standalone-social-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^1.27.0",
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
"devDependencies": {
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"oxlint": "^1.71.0",
"vite": "^8.1.1"
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

View File

@@ -0,0 +1,33 @@
import React, { useState } from 'react';
import Sidebar from './components/Sidebar';
import Header from './components/Header';
import Dashboard from './components/Dashboard';
import PostModal from './components/PostModal';
import './index.css';
function App() {
const [activeTab, setActiveTab] = useState('dashboard');
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div className="app-container">
<Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
<main className="main-content">
<Header onNewPost={() => setIsModalOpen(true)} />
{activeTab === 'dashboard' || activeTab === 'feed' ? (
<Dashboard />
) : (
<div style={{ padding: '2rem', display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--text-muted)' }}>
<h2>{activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} Module (Coming Soon)</h2>
</div>
)}
</main>
{isModalOpen && <PostModal onClose={() => setIsModalOpen(false)} />}
</div>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,128 @@
import React from 'react';
import { MoreHorizontal, MessageCircle, Heart, Share, BarChart2, Eye, MousePointerClick, Users } from 'lucide-react';
const StreamColumn = ({ title, platform, posts }) => {
return (
<div className="stream-column">
<div className="stream-header">
<div className="stream-title">
<div className={`platform-badge platform-${platform}`}>
<span style={{ fontSize: '0.8rem', fontWeight: 'bold' }}>
{platform.charAt(0).toUpperCase()}
</span>
</div>
<span>{title}</span>
</div>
<button style={{ background: 'transparent', border: 'none', color: 'var(--text-muted)', cursor: 'pointer' }}>
<MoreHorizontal size={20} />
</button>
</div>
<div className="stream-content">
{posts.map(post => (
<div key={post.id} className="post-card">
<div className="post-header">
<div className="post-author">
<img src={post.avatar} alt="avatar" className="avatar" />
<div className="author-info">
<h4>{post.author}</h4>
<span>{post.time}</span>
</div>
</div>
<button style={{ background: 'transparent', border: 'none', color: 'var(--text-muted)', cursor: 'pointer' }}>
<MoreHorizontal size={16} />
</button>
</div>
<p className="post-text">{post.content}</p>
{post.image && <img src={post.image} alt="post media" className="post-image" />}
<div className="post-actions">
<button className="action-btn">
<Heart size={16} /> {post.likes}
</button>
<button className="action-btn">
<MessageCircle size={16} /> {post.comments}
</button>
<button className="action-btn">
<Share size={16} /> {post.shares}
</button>
</div>
</div>
))}
</div>
</div>
);
};
const Dashboard = () => {
const dummyPosts = [
{
id: 1,
author: 'Odoo Social',
time: '2 hours ago',
avatar: 'https://i.pravatar.cc/150?u=odoo',
content: 'Excited to announce our new upcoming features for the social marketing module! 🚀 #Odoo19 #Marketing',
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=60',
likes: 124,
comments: 18,
shares: 5
},
{
id: 2,
author: 'Jane Doe',
time: '5 hours ago',
avatar: 'https://i.pravatar.cc/150?u=jane',
content: 'Just published a new blog post on how to optimize your digital campaigns.',
likes: 45,
comments: 3,
shares: 12
}
];
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div className="metric-cards">
<div className="metric-card">
<div className="metric-icon"><Users size={24} /></div>
<div className="metric-info">
<h3>24.5k</h3>
<p>Total Audience</p>
</div>
</div>
<div className="metric-card">
<div className="metric-icon"><Eye size={24} /></div>
<div className="metric-info">
<h3>142.1k</h3>
<p>Impressions</p>
</div>
</div>
<div className="metric-card">
<div className="metric-icon"><MousePointerClick size={24} /></div>
<div className="metric-info">
<h3>12.4k</h3>
<p>Engagements</p>
</div>
</div>
<div className="metric-card">
<div className="metric-icon"><BarChart2 size={24} /></div>
<div className="metric-info">
<h3>8.2%</h3>
<p>Engagement Rate</p>
</div>
</div>
</div>
<div className="dashboard-scroll">
<div className="streams-container">
<StreamColumn title="Company Page" platform="facebook" posts={dummyPosts} />
<StreamColumn title="Official Twitter" platform="twitter" posts={[dummyPosts[0]]} />
<StreamColumn title="LinkedIn Corporate" platform="linkedin" posts={dummyPosts} />
<StreamColumn title="Instagram Feed" platform="instagram" posts={[dummyPosts[1]]} />
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { Plus, Bell, Search, User } from 'lucide-react';
const Header = ({ onNewPost }) => {
return (
<header className="topbar">
<div className="search-bar" style={{ display: 'flex', alignItems: 'center', background: 'var(--bg-primary)', padding: '0.5rem 1rem', borderRadius: '2rem', border: '1px solid var(--border-color)', width: '300px' }}>
<Search size={18} color="var(--text-muted)" style={{ marginRight: '0.5rem' }} />
<input
type="text"
placeholder="Search posts, campaigns..."
style={{ background: 'transparent', border: 'none', color: 'var(--text-primary)', outline: 'none', width: '100%' }}
/>
</div>
<div className="user-menu">
<button className="btn btn-primary" onClick={onNewPost}>
<Plus size={18} />
New Post
</button>
<div style={{ position: 'relative', cursor: 'pointer' }}>
<Bell size={20} color="var(--text-secondary)" />
<span style={{ position: 'absolute', top: '-2px', right: '-2px', background: 'var(--accent-primary)', width: '8px', height: '8px', borderRadius: '50%' }}></span>
</div>
<div className="avatar" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--accent-light)', color: 'var(--accent-primary)', cursor: 'pointer' }}>
<User size={20} />
</div>
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,95 @@
import React, { useState } from 'react';
import { X, Image as ImageIcon, Link as LinkIcon, Calendar, Check } from 'lucide-react';
const PostModal = ({ onClose }) => {
const [message, setMessage] = useState('');
const [platforms, setPlatforms] = useState({
facebook: true,
twitter: true,
linkedin: false,
instagram: false
});
const togglePlatform = (platform) => {
setPlatforms(prev => ({ ...prev, [platform]: !prev[platform] }));
};
const handlePost = (e) => {
e.preventDefault();
// Simulate posting
setTimeout(() => {
onClose();
}, 600);
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 style={{ fontSize: '1.25rem', fontWeight: 600 }}>Create New Post</h2>
<button onClick={onClose} style={{ background: 'transparent', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer' }}>
<X size={24} />
</button>
</div>
<form onSubmit={handlePost}>
<div className="modal-body">
<div className="form-group">
<label>Select Platforms</label>
<div className="platform-select">
{['facebook', 'twitter', 'linkedin', 'instagram'].map(platform => (
<div
key={platform}
className={`platform-checkbox ${platforms[platform] ? 'selected' : ''}`}
onClick={() => togglePlatform(platform)}
>
<div className={`platform-badge platform-${platform}`}>
{/* Using first letter as a simple logo replacement for now */}
<span style={{ fontSize: '0.8rem', fontWeight: 'bold' }}>
{platform.charAt(0).toUpperCase()}
</span>
</div>
<span style={{ fontSize: '0.875rem', textTransform: 'capitalize' }}>{platform}</span>
{platforms[platform] && <Check size={16} color="var(--accent-primary)" style={{ marginLeft: 'auto' }} />}
</div>
))}
</div>
</div>
<div className="form-group">
<label>Message</label>
<textarea
className="form-control"
placeholder="What do you want to share?"
value={message}
onChange={e => setMessage(e.target.value)}
required
/>
</div>
<div className="flex gap-4">
<button type="button" className="action-btn">
<ImageIcon size={18} />
<span>Attach Image</span>
</button>
<button type="button" className="action-btn">
<LinkIcon size={18} />
<span>Add Link</span>
</button>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={onClose}>Cancel</button>
<button type="button" className="btn btn-secondary flex items-center gap-2">
<Calendar size={16} /> Schedule
</button>
<button type="submit" className="btn btn-primary">Post Now</button>
</div>
</form>
</div>
</div>
);
};
export default PostModal;

View File

@@ -0,0 +1,48 @@
import React from 'react';
import {
LayoutDashboard,
Share2,
Calendar,
MessageCircle,
Users,
Settings,
Activity
} from 'lucide-react';
const Sidebar = ({ activeTab, setActiveTab }) => {
const navItems = [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'feed', label: 'Feed', icon: Activity },
{ id: 'posts', label: 'Posts', icon: Share2 },
{ id: 'calendar', label: 'Calendar', icon: Calendar },
{ id: 'visitors', label: 'Visitors', icon: Users },
{ id: 'chat', label: 'Live Chat', icon: MessageCircle },
{ id: 'settings', label: 'Configuration', icon: Settings },
];
return (
<aside className="sidebar">
<div className="brand">
<div className="brand-icon">
<Share2 size={24} color="white" />
</div>
<span>Odoo Social</span>
</div>
<nav className="nav-menu">
{navItems.map((item) => (
<div
key={item.id}
className={`nav-item ${activeTab === item.id ? 'active' : ''}`}
onClick={() => setActiveTab(item.id)}
>
<item.icon size={20} />
<span>{item.label}</span>
</div>
))}
</nav>
</aside>
);
};
export default Sidebar;

View File

@@ -0,0 +1,482 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f8fafc;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
--accent-primary: #6366f1;
--accent-hover: #4f46e5;
--accent-light: rgba(99, 102, 241, 0.15);
--border-color: rgba(255, 255, 255, 0.1);
--glass-bg: rgba(30, 41, 59, 0.7);
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* Layout */
.app-container {
display: flex;
height: 100vh;
overflow: hidden;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: 260px;
background: var(--glass-bg);
backdrop-filter: blur(12px);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 1.5rem;
z-index: 10;
}
.brand {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 2.5rem;
}
.brand-icon {
background: linear-gradient(135deg, var(--accent-primary), #a855f7);
padding: 0.5rem;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
}
.nav-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-radius: var(--radius-md);
color: var(--text-secondary);
font-weight: 500;
cursor: pointer;
transition: var(--transition);
margin-bottom: 0.25rem;
}
.nav-item:hover, .nav-item.active {
background: var(--accent-light);
color: var(--accent-primary);
transform: translateX(4px);
}
/* Header */
.topbar {
height: 72px;
background: var(--glass-bg);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2rem;
z-index: 10;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 1.25rem;
border-radius: var(--radius-md);
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: var(--transition);
border: none;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-primary), #a855f7);
color: white;
box-shadow: 0 4px 14px 0 rgba(99, 102, 241, 0.39);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.5);
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-secondary:hover {
background: var(--bg-secondary);
}
/* Dashboard & Streams */
.dashboard-scroll {
flex: 1;
overflow-x: auto;
overflow-y: hidden;
padding: 2rem;
}
.streams-container {
display: flex;
gap: 1.5rem;
height: 100%;
}
.stream-column {
width: 380px;
min-width: 380px;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
overflow: hidden;
}
.stream-header {
padding: 1.25rem;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.stream-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
}
.stream-content {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.stream-content::-webkit-scrollbar {
width: 6px;
}
.stream-content::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 10px;
}
/* Post Cards */
.post-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 1.25rem;
transition: var(--transition);
}
.post-card:hover {
transform: translateY(-2px);
border-color: var(--text-muted);
box-shadow: var(--shadow-md);
}
.post-header {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
}
.post-author {
display: flex;
align-items: center;
gap: 0.75rem;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg-tertiary);
object-fit: cover;
}
.author-info h4 {
font-size: 0.875rem;
color: var(--text-primary);
}
.author-info span {
font-size: 0.75rem;
color: var(--text-muted);
}
.post-text {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 1rem;
}
.post-image {
width: 100%;
border-radius: var(--radius-md);
margin-bottom: 1rem;
}
.post-actions {
display: flex;
gap: 1rem;
border-top: 1px solid var(--border-color);
padding-top: 0.75rem;
}
.action-btn {
display: flex;
align-items: center;
gap: 0.5rem;
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 0.875rem;
transition: var(--transition);
}
.action-btn:hover {
color: var(--accent-primary);
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
animation: fadeIn 0.2s ease-out;
}
.modal-content {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-xl);
width: 100%;
max-width: 600px;
box-shadow: var(--shadow-lg);
animation: slideUp 0.3s ease-out;
overflow: hidden;
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
padding: 1.5rem;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
.form-control {
width: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 0.75rem;
color: var(--text-primary);
font-family: inherit;
font-size: 0.875rem;
transition: var(--transition);
}
.form-control:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px var(--accent-light);
}
textarea.form-control {
min-height: 120px;
resize: vertical;
}
/* Utils */
.flex { display: flex; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: 0.5rem; }
.gap-4 { gap: 1rem; }
.text-xs { font-size: 0.75rem; }
.text-sm { font-size: 0.875rem; }
.text-muted { color: var(--text-muted); }
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* User Profile Mini */
.user-menu {
display: flex;
align-items: center;
gap: 1rem;
}
.metric-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
margin-bottom: 2rem;
padding: 0 2rem;
margin-top: 2rem;
}
.metric-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1.25rem;
transition: var(--transition);
}
.metric-card:hover {
transform: translateY(-3px);
border-color: var(--accent-primary);
box-shadow: 0 10px 25px -5px rgba(99, 102, 241, 0.15);
}
.metric-icon {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-primary);
}
.metric-info h3 {
font-size: 1.5rem;
margin: 0;
}
.metric-info p {
color: var(--text-muted);
font-size: 0.875rem;
margin: 0;
}
.platform-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
color: white;
}
.platform-facebook { background-color: #1877F2; }
.platform-twitter { background-color: #1DA1F2; }
.platform-linkedin { background-color: #0A66C2; }
.platform-instagram { background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%); }
.platform-select {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.platform-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
padding: 0.5rem 1rem;
border-radius: 2rem;
cursor: pointer;
transition: var(--transition);
}
.platform-checkbox:hover {
border-color: var(--accent-primary);
}
.platform-checkbox.selected {
background: var(--accent-light);
border-color: var(--accent-primary);
}

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})